From d16bd1b3c4fffe03756637eb66bfc8eb3b3072f1 Mon Sep 17 00:00:00 2001 From: Tom Hempel Date: Mon, 8 Jun 2026 19:44:58 +0200 Subject: [PATCH] test data import fix for scoring --- db_init.php | 12 +++++++- lib/dev_fixture.php | 17 +++++++++++ lib/scoring.php | 51 ++++++++++++++++++++++++--------- scripts/generate_dev_fixture.py | 25 +++++++++++----- website/js/pages/dev.js | 2 +- 5 files changed, 85 insertions(+), 22 deletions(-) diff --git a/db_init.php b/db_init.php index b10351d..b5a567f 100644 --- a/db_init.php +++ b/db_init.php @@ -13,7 +13,7 @@ if (defined('QDB_TEST_UPLOADS')) { define('QDB_LOCK', QDB_UPLOADS_DIR . '/.qdb_lock'); } define('QDB_SCHEMA', __DIR__ . '/schema.sql'); -define('QDB_VERSION', 9); +define('QDB_VERSION', 10); function qdb_table_exists(PDO $pdo, string $table): bool { $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(); if ($currentVersion < 7 && qdb_table_exists($pdo, 'question_score_rule')) { require_once __DIR__ . '/lib/scoring.php'; diff --git a/lib/dev_fixture.php b/lib/dev_fixture.php index 0385b68..6107c41 100644 --- a/lib/dev_fixture.php +++ b/lib/dev_fixture.php @@ -16,6 +16,7 @@ const QDB_DEV_GLASS_POINTS = [ require_once __DIR__ . '/app_answers.php'; require_once __DIR__ . '/submissions.php'; +require_once __DIR__ . '/scoring.php'; /** * @return array{imported: array, skipped: array, errors: string[]} @@ -36,6 +37,7 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array 'completions' => 0, 'submissions' => 0, 'answers' => 0, + 'scoringRecomputed' => 0, ]; $skipped = [ 'admins' => 0, @@ -48,6 +50,7 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array $coachIdByUsername = []; $supervisorIdByUsername = []; + $affectedClients = []; $hash = password_hash($password, PASSWORD_DEFAULT); $now = time(); @@ -218,9 +221,23 @@ function qdb_import_dev_fixture(PDO $pdo, array $fixture): array } $imported['completions']++; + $affectedClients[$clientCode] = true; $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(); } catch (Throwable $e) { if ($pdo->inTransaction()) { diff --git a/lib/scoring.php b/lib/scoring.php index 99e429d..2524a78 100644 --- a/lib/scoring.php +++ b/lib/scoring.php @@ -534,27 +534,52 @@ function qdb_backfill_glass_score_rules(PDO $pdo): int { return $count; } -function qdb_recompute_all_questionnaire_scores(PDO $pdo): int { - $stmt = $pdo->query( - "SELECT clientCode, questionnaireID FROM completed_questionnaire WHERE status = 'completed'" - ); +/** + * Recompute questionnaire sumPoints and scoring-profile results for specific clients. + * + * @param list $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; + $qnStmt = $pdo->prepare( + "SELECT questionnaireID FROM completed_questionnaire + WHERE clientCode = :cc AND status = 'completed'" + ); $upd = $pdo->prepare( 'UPDATE completed_questionnaire SET sumPoints = :sp WHERE clientCode = :cc AND questionnaireID = :qn' ); - foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) { - $score = qdb_compute_questionnaire_score($pdo, $row['clientCode'], $row['questionnaireID']); - $upd->execute([ - ':sp' => $score, - ':cc' => $row['clientCode'], - ':qn' => $row['questionnaireID'], - ]); - qdb_recompute_profile_scores_for_client($pdo, $row['clientCode']); - $n++; + foreach ($clientCodes as $clientCode) { + $qnStmt->execute([':cc' => $clientCode]); + foreach ($qnStmt->fetchAll(PDO::FETCH_COLUMN) as $qnID) { + $score = qdb_compute_questionnaire_score($pdo, $clientCode, (string)$qnID); + $upd->execute([ + ':sp' => $score, + ':cc' => $clientCode, + ':qn' => $qnID, + ]); + $n++; + } + qdb_recompute_profile_scores_for_client($pdo, $clientCode); } 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 { foreach (qdb_list_scoring_profiles($pdo) as $p) { if ($p['name'] === 'RHS Index') { diff --git a/scripts/generate_dev_fixture.py b/scripts/generate_dev_fixture.py index 458d191..4235f8e 100644 --- a/scripts/generate_dev_fixture.py +++ b/scripts/generate_dev_fixture.py @@ -14,7 +14,7 @@ SCALE = 5 RNG = random.Random(42) 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 = [ "questionnaire_1_demographic_information", @@ -35,10 +35,20 @@ CLIENTS_WITHOUT_COMPLETIONS = 15 * SCALE COMPLETION_PARTICIPATION = 0.82 -def load_questionnaire_ids() -> list[str]: - if not BUNDLE_PATH.is_file(): +def resolve_bundle_path() -> Path | None: + """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 - bundle = json.loads(BUNDLE_PATH.read_text(encoding="utf-8")) + bundle = json.loads(bundle_path.read_text(encoding="utf-8")) ids = [ item["questionnaire"]["questionnaireID"] for item in bundle.get("questionnaires", []) @@ -153,7 +163,8 @@ def build_completions( def main(): - questionnaire_ids = load_questionnaire_ids() + bundle_path = resolve_bundle_path() + questionnaire_ids = load_questionnaire_ids(bundle_path) admins = [ {"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) 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 = { "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") print(f"Wrote {out}") print(json.dumps(fixture["stats"], indent=2)) diff --git a/website/js/pages/dev.js b/website/js/pages/dev.js index 6cc4f53..3a28654 100644 --- a/website/js/pages/dev.js +++ b/website/js/pages/dev.js @@ -71,7 +71,7 @@ export function devPage() {
- Use dev-test-fixture.json from the repo root (generate via nat-as-server/scripts/generate_dev_fixture.py; uneven coach load, multi-version uploads, natural timestamps). + Use dev-test-fixture.json from the repo root (regenerate with nat-as-server/scripts/generate_dev_fixture.py after importing the latest questionnaires_bundle_*.json; uneven coach load, multi-version uploads, natural timestamps).