This commit is contained in:
12
db_init.php
12
db_init.php
@ -13,7 +13,7 @@ if (defined('QDB_TEST_UPLOADS')) {
|
|||||||
define('QDB_LOCK', QDB_UPLOADS_DIR . '/.qdb_lock');
|
define('QDB_LOCK', QDB_UPLOADS_DIR . '/.qdb_lock');
|
||||||
}
|
}
|
||||||
define('QDB_SCHEMA', __DIR__ . '/schema.sql');
|
define('QDB_SCHEMA', __DIR__ . '/schema.sql');
|
||||||
define('QDB_VERSION', 9);
|
define('QDB_VERSION', 10);
|
||||||
|
|
||||||
function qdb_table_exists(PDO $pdo, string $table): bool {
|
function qdb_table_exists(PDO $pdo, string $table): bool {
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
@ -219,6 +219,16 @@ function qdb_apply_migrations(PDO $pdo, string $schemaSql): bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$currentVersion = (int)$pdo->query('PRAGMA user_version')->fetchColumn();
|
||||||
|
if ($currentVersion < 10
|
||||||
|
&& qdb_table_exists($pdo, 'completed_questionnaire')
|
||||||
|
&& qdb_table_exists($pdo, 'client_scoring_profile_result')) {
|
||||||
|
require_once __DIR__ . '/lib/scoring.php';
|
||||||
|
if (qdb_recompute_all_questionnaire_scores($pdo) > 0) {
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$currentVersion = (int)$pdo->query('PRAGMA user_version')->fetchColumn();
|
$currentVersion = (int)$pdo->query('PRAGMA user_version')->fetchColumn();
|
||||||
if ($currentVersion < 7 && qdb_table_exists($pdo, 'question_score_rule')) {
|
if ($currentVersion < 7 && qdb_table_exists($pdo, 'question_score_rule')) {
|
||||||
require_once __DIR__ . '/lib/scoring.php';
|
require_once __DIR__ . '/lib/scoring.php';
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const QDB_DEV_GLASS_POINTS = [
|
|||||||
|
|
||||||
require_once __DIR__ . '/app_answers.php';
|
require_once __DIR__ . '/app_answers.php';
|
||||||
require_once __DIR__ . '/submissions.php';
|
require_once __DIR__ . '/submissions.php';
|
||||||
|
require_once __DIR__ . '/scoring.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{imported: array, skipped: array, errors: string[]}
|
* @return array{imported: array, skipped: array, errors: string[]}
|
||||||
@ -36,6 +37,7 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array
|
|||||||
'completions' => 0,
|
'completions' => 0,
|
||||||
'submissions' => 0,
|
'submissions' => 0,
|
||||||
'answers' => 0,
|
'answers' => 0,
|
||||||
|
'scoringRecomputed' => 0,
|
||||||
];
|
];
|
||||||
$skipped = [
|
$skipped = [
|
||||||
'admins' => 0,
|
'admins' => 0,
|
||||||
@ -48,6 +50,7 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array
|
|||||||
|
|
||||||
$coachIdByUsername = [];
|
$coachIdByUsername = [];
|
||||||
$supervisorIdByUsername = [];
|
$supervisorIdByUsername = [];
|
||||||
|
$affectedClients = [];
|
||||||
|
|
||||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
$now = time();
|
$now = time();
|
||||||
@ -218,9 +221,23 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$imported['completions']++;
|
$imported['completions']++;
|
||||||
|
$affectedClients[$clientCode] = true;
|
||||||
$variant++;
|
$variant++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$upperPrefix = strtoupper(rtrim($prefix, '_'));
|
||||||
|
$devClientStmt = $pdo->prepare('SELECT clientCode FROM client WHERE clientCode LIKE :pfx');
|
||||||
|
$devClientStmt->execute([':pfx' => $upperPrefix . '%']);
|
||||||
|
foreach ($devClientStmt->fetchAll(PDO::FETCH_COLUMN) as $cc) {
|
||||||
|
$affectedClients[(string)$cc] = true;
|
||||||
|
}
|
||||||
|
if ($affectedClients !== []) {
|
||||||
|
$imported['scoringRecomputed'] = qdb_recompute_scores_for_clients(
|
||||||
|
$pdo,
|
||||||
|
array_keys($affectedClients)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
if ($pdo->inTransaction()) {
|
if ($pdo->inTransaction()) {
|
||||||
|
|||||||
@ -534,27 +534,52 @@ function qdb_backfill_glass_score_rules(PDO $pdo): int {
|
|||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
function qdb_recompute_all_questionnaire_scores(PDO $pdo): int {
|
/**
|
||||||
$stmt = $pdo->query(
|
* Recompute questionnaire sumPoints and scoring-profile results for specific clients.
|
||||||
"SELECT clientCode, questionnaireID FROM completed_questionnaire WHERE status = 'completed'"
|
*
|
||||||
);
|
* @param list<string> $clientCodes
|
||||||
|
*/
|
||||||
|
function qdb_recompute_scores_for_clients(PDO $pdo, array $clientCodes): int {
|
||||||
|
$clientCodes = array_values(array_unique(array_filter(
|
||||||
|
$clientCodes,
|
||||||
|
static fn($cc) => is_string($cc) && trim($cc) !== ''
|
||||||
|
)));
|
||||||
|
if ($clientCodes === []) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
$n = 0;
|
$n = 0;
|
||||||
|
$qnStmt = $pdo->prepare(
|
||||||
|
"SELECT questionnaireID FROM completed_questionnaire
|
||||||
|
WHERE clientCode = :cc AND status = 'completed'"
|
||||||
|
);
|
||||||
$upd = $pdo->prepare(
|
$upd = $pdo->prepare(
|
||||||
'UPDATE completed_questionnaire SET sumPoints = :sp WHERE clientCode = :cc AND questionnaireID = :qn'
|
'UPDATE completed_questionnaire SET sumPoints = :sp WHERE clientCode = :cc AND questionnaireID = :qn'
|
||||||
);
|
);
|
||||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
foreach ($clientCodes as $clientCode) {
|
||||||
$score = qdb_compute_questionnaire_score($pdo, $row['clientCode'], $row['questionnaireID']);
|
$qnStmt->execute([':cc' => $clientCode]);
|
||||||
$upd->execute([
|
foreach ($qnStmt->fetchAll(PDO::FETCH_COLUMN) as $qnID) {
|
||||||
':sp' => $score,
|
$score = qdb_compute_questionnaire_score($pdo, $clientCode, (string)$qnID);
|
||||||
':cc' => $row['clientCode'],
|
$upd->execute([
|
||||||
':qn' => $row['questionnaireID'],
|
':sp' => $score,
|
||||||
]);
|
':cc' => $clientCode,
|
||||||
qdb_recompute_profile_scores_for_client($pdo, $row['clientCode']);
|
':qn' => $qnID,
|
||||||
$n++;
|
]);
|
||||||
|
$n++;
|
||||||
|
}
|
||||||
|
qdb_recompute_profile_scores_for_client($pdo, $clientCode);
|
||||||
}
|
}
|
||||||
return $n;
|
return $n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function qdb_recompute_all_questionnaire_scores(PDO $pdo): int {
|
||||||
|
$stmt = $pdo->query(
|
||||||
|
"SELECT DISTINCT clientCode FROM completed_questionnaire WHERE status = 'completed'"
|
||||||
|
);
|
||||||
|
$clientCodes = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
return qdb_recompute_scores_for_clients($pdo, $clientCodes);
|
||||||
|
}
|
||||||
|
|
||||||
function qdb_seed_default_rhs_scoring_profile(PDO $pdo): ?string {
|
function qdb_seed_default_rhs_scoring_profile(PDO $pdo): ?string {
|
||||||
foreach (qdb_list_scoring_profiles($pdo) as $p) {
|
foreach (qdb_list_scoring_profiles($pdo) as $p) {
|
||||||
if ($p['name'] === 'RHS Index') {
|
if ($p['name'] === 'RHS Index') {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ SCALE = 5
|
|||||||
RNG = random.Random(42)
|
RNG = random.Random(42)
|
||||||
ANCHOR = datetime(2026, 5, 27, 15, 30, 0)
|
ANCHOR = datetime(2026, 5, 27, 15, 30, 0)
|
||||||
|
|
||||||
BUNDLE_PATH = Path(__file__).resolve().parents[2] / "questionnaires_bundle_2026-05-28.json"
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
FALLBACK_QUESTIONNAIRE_IDS = [
|
FALLBACK_QUESTIONNAIRE_IDS = [
|
||||||
"questionnaire_1_demographic_information",
|
"questionnaire_1_demographic_information",
|
||||||
@ -35,10 +35,20 @@ CLIENTS_WITHOUT_COMPLETIONS = 15 * SCALE
|
|||||||
COMPLETION_PARTICIPATION = 0.82
|
COMPLETION_PARTICIPATION = 0.82
|
||||||
|
|
||||||
|
|
||||||
def load_questionnaire_ids() -> list[str]:
|
def resolve_bundle_path() -> Path | None:
|
||||||
if not BUNDLE_PATH.is_file():
|
"""Use the newest questionnaires_bundle_*.json in the repo root."""
|
||||||
|
candidates = sorted(
|
||||||
|
REPO_ROOT.glob("questionnaires_bundle_*.json"),
|
||||||
|
key=lambda p: p.stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def load_questionnaire_ids(bundle_path: Path | None) -> list[str]:
|
||||||
|
if bundle_path is None or not bundle_path.is_file():
|
||||||
return FALLBACK_QUESTIONNAIRE_IDS
|
return FALLBACK_QUESTIONNAIRE_IDS
|
||||||
bundle = json.loads(BUNDLE_PATH.read_text(encoding="utf-8"))
|
bundle = json.loads(bundle_path.read_text(encoding="utf-8"))
|
||||||
ids = [
|
ids = [
|
||||||
item["questionnaire"]["questionnaireID"]
|
item["questionnaire"]["questionnaireID"]
|
||||||
for item in bundle.get("questionnaires", [])
|
for item in bundle.get("questionnaires", [])
|
||||||
@ -153,7 +163,8 @@ def build_completions(
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
questionnaire_ids = load_questionnaire_ids()
|
bundle_path = resolve_bundle_path()
|
||||||
|
questionnaire_ids = load_questionnaire_ids(bundle_path)
|
||||||
|
|
||||||
admins = [
|
admins = [
|
||||||
{"username": f"{PREFIX}admin_{i}", "location": f"Dev Admin Standort {i}"}
|
{"username": f"{PREFIX}admin_{i}", "location": f"Dev Admin Standort {i}"}
|
||||||
@ -185,7 +196,7 @@ def main():
|
|||||||
total_uploads = sum(c["versions"] for c in completions)
|
total_uploads = sum(c["versions"] for c in completions)
|
||||||
multi_version_pairs = sum(1 for c in completions if c["versions"] > 1)
|
multi_version_pairs = sum(1 for c in completions if c["versions"] > 1)
|
||||||
|
|
||||||
bundle_note = BUNDLE_PATH.name if BUNDLE_PATH.is_file() else "fallback questionnaire list"
|
bundle_note = bundle_path.name if bundle_path and bundle_path.is_file() else "fallback questionnaire list"
|
||||||
|
|
||||||
fixture = {
|
fixture = {
|
||||||
"fixtureVersion": 2,
|
"fixtureVersion": 2,
|
||||||
@ -216,7 +227,7 @@ def main():
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out = Path(__file__).resolve().parents[2] / "dev-test-fixture.json"
|
out = REPO_ROOT / "dev-test-fixture.json"
|
||||||
out.write_text(json.dumps(fixture, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
out.write_text(json.dumps(fixture, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
print(f"Wrote {out}")
|
print(f"Wrote {out}")
|
||||||
print(json.dumps(fixture["stats"], indent=2))
|
print(json.dumps(fixture["stats"], indent=2))
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export function devPage() {
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="devFixtureFile">Fixture JSON</label>
|
<label for="devFixtureFile">Fixture JSON</label>
|
||||||
<input type="file" id="devFixtureFile" accept=".json,application/json">
|
<input type="file" id="devFixtureFile" accept=".json,application/json">
|
||||||
<span class="field-hint">Use <code>dev-test-fixture.json</code> from the repo root (generate via <code>nat-as-server/scripts/generate_dev_fixture.py</code>; uneven coach load, multi-version uploads, natural timestamps).</span>
|
<span class="field-hint">Use <code>dev-test-fixture.json</code> from the repo root (regenerate with <code>nat-as-server/scripts/generate_dev_fixture.py</code> after importing the latest <code>questionnaires_bundle_*.json</code>; uneven coach load, multi-version uploads, natural timestamps).</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="devFixtureSummary" class="field-hint" style="margin:12px 0;display:none"></div>
|
<div id="devFixtureSummary" class="field-hint" style="margin:12px 0;display:none"></div>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px">
|
||||||
|
|||||||
Reference in New Issue
Block a user