feat(progress): rekap distribusi bobot jawaban (poin 5)
Hitung ulang distribusi point N: count dari jawaban, tampilkan di ringkasan siswa, tooltip guru, dan CSV.
This commit is contained in:
parent
ec34ffed72
commit
2a5fdf8400
|
|
@ -6,6 +6,7 @@ import {
|
|||
firstUnansweredIndex,
|
||||
answerQuestion,
|
||||
questionMaxPoints,
|
||||
weightedPointDistribution,
|
||||
QUIZ_PASS_THRESHOLD,
|
||||
} from './quiz-session';
|
||||
import type { QuizQuestion, QuizOption } from '../types/quiz';
|
||||
|
|
@ -301,3 +302,56 @@ describe('Fitur B — model skor rata-rata per jenis', () => {
|
|||
expect(calculateQuizResult(sess).statusString).toBe('1/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('weightedPointDistribution (rekap counting bobot)', () => {
|
||||
it('menghitung per bobot terpilih, urut descending, unanswered terpisah', () => {
|
||||
const sess = createQuizSession(
|
||||
[
|
||||
weighted('w-0', [10, 7, null]),
|
||||
weighted('w-1', [10, 7, null]),
|
||||
weighted('w-2', [7, 3, null]),
|
||||
weighted('w-3', [5, null]),
|
||||
],
|
||||
() => 0
|
||||
);
|
||||
answerQuestion(sess, 'w-0', 'w-0-o-0'); // 10
|
||||
answerQuestion(sess, 'w-1', 'w-1-o-1'); // 7
|
||||
answerQuestion(sess, 'w-2', 'w-2-o-0'); // 7
|
||||
// w-3 tidak dijawab
|
||||
|
||||
expect(weightedPointDistribution(sess.questions, sess.answers)).toEqual({
|
||||
points: [
|
||||
{ weight: 10, count: 1 },
|
||||
{ weight: 7, count: 2 },
|
||||
],
|
||||
unanswered: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('opsi bobot 0 yang dipilih tercatat sebagai point 0, bukan unanswered', () => {
|
||||
const sess = createQuizSession([weighted('w-0', [10, 0, null])], () => 0);
|
||||
answerQuestion(sess, 'w-0', 'w-0-o-1');
|
||||
expect(weightedPointDistribution(sess.questions, sess.answers)).toEqual({
|
||||
points: [{ weight: 0, count: 1 }],
|
||||
unanswered: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('soal non-weighted diabaikan', () => {
|
||||
const sess = createQuizSession([mcq('m-0', 2), weighted('w-0', [5, null])], () => 0);
|
||||
answerQuestion(sess, 'm-0', 'm-0-o-0');
|
||||
answerQuestion(sess, 'w-0', 'w-0-o-0');
|
||||
expect(weightedPointDistribution(sess.questions, sess.answers)).toEqual({
|
||||
points: [{ weight: 5, count: 1 }],
|
||||
unanswered: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('semua weighted belum dijawab → points kosong, unanswered = jumlah soal', () => {
|
||||
const sess = createQuizSession([weighted('w-0', [10, null]), weighted('w-1', [7, null])], () => 0);
|
||||
expect(weightedPointDistribution(sess.questions, sess.answers)).toEqual({
|
||||
points: [],
|
||||
unanswered: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -234,8 +234,46 @@ export function calculateQuizResult(session: QuizSession): QuizResult {
|
|||
};
|
||||
}
|
||||
|
||||
/** Ringkasan jawaban tersimpan dari attempt sebelumnya (payload backend). */
|
||||
export interface StoredQuizAnswer {
|
||||
/** Distribusi bobot jawaban soal weighted — dihitung ulang dari konten. */
|
||||
export interface WeightedPointDistribution {
|
||||
/** Bobot terpilih → jumlah soal, urut bobot tertinggi → terendah. */
|
||||
points: Array<{ weight: number; count: number }>;
|
||||
/** Soal weighted yang belum dijawab (bukan `point 0`). */
|
||||
unanswered: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rekap counting jawaban soal berbobot: `point <bobot>: <jumlah>`.
|
||||
* Soal yang belum dijawab dipisah agar tidak tercampur dengan `point 0`
|
||||
* (opsi berbobot 0 yang benar-benar dipilih).
|
||||
*/
|
||||
export function weightedPointDistribution(
|
||||
questions: QuizQuestion[],
|
||||
answers: Record<string, QuizAnswer>
|
||||
): WeightedPointDistribution {
|
||||
const counts = new Map<number, number>();
|
||||
let unanswered = 0;
|
||||
|
||||
for (const q of questions) {
|
||||
if (q.type !== 'mcq' || q.quiz_type !== 'weighted') continue;
|
||||
const a = answers[q.id];
|
||||
if (!a || a.selectedOptionId === null) {
|
||||
unanswered += 1;
|
||||
continue;
|
||||
}
|
||||
const weight = a.earnedPoints ?? 0;
|
||||
counts.set(weight, (counts.get(weight) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
points: [...counts.entries()]
|
||||
.sort((a, b) => b[0] - a[0])
|
||||
.map(([weight, count]) => ({ weight, count })),
|
||||
unanswered
|
||||
};
|
||||
}
|
||||
|
||||
/** Ringkasan jawaban tersimpan dari attempt sebelumnya (payload backend). */export interface StoredQuizAnswer {
|
||||
question_id: string;
|
||||
selected_option_id: string | null;
|
||||
is_correct?: boolean;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
import { autoRenderMath } from '$lib/actions/renderMath';
|
||||
import { shouldShowQuizReview, type QuizTerminationReason } from '$services/quiz-integrity';
|
||||
import { resolveQuizTabView, terminationReasonLabel } from '$services/quiz-view';
|
||||
import { questionMaxPoints, QUIZ_PASS_THRESHOLD } from '$services/quiz-session';
|
||||
import { questionMaxPoints, weightedPointDistribution, QUIZ_PASS_THRESHOLD } from '$services/quiz-session';
|
||||
import type { WeightedPointDistribution } from '$services/quiz-session';
|
||||
import type { LessonManager } from './lesson.svelte';
|
||||
import type { QuizQuestion, QuizAnswer } from '$types/quiz';
|
||||
import { fetchLessonProgress } from '$services/api';
|
||||
|
|
@ -91,6 +92,18 @@
|
|||
const currentAnswer = $derived(mgr.currentQuizAnswer);
|
||||
const questions = $derived(mgr.quizSession?.questions ?? []);
|
||||
const result = $derived(mgr.quizResult);
|
||||
const resultWeightedDistribution = $derived(
|
||||
weightedPointDistribution(questions, mgr.quizSession?.answers ?? {})
|
||||
);
|
||||
const hasResultWeightedQuestions = $derived(
|
||||
questions.some((q) => q.type === 'mcq' && q.quiz_type === 'weighted')
|
||||
);
|
||||
const storedWeightedDistribution = $derived(
|
||||
weightedPointDistribution(reviewQuestions, reviewAnswers)
|
||||
);
|
||||
const hasStoredWeightedQuestions = $derived(
|
||||
reviewQuestions.some((q) => q.type === 'mcq' && q.quiz_type === 'weighted')
|
||||
);
|
||||
// Strict policy: kuis yang dihentikan karena focus_lost tidak menampilkan
|
||||
// pembahasan (siswa yang kehilangan fokus tidak mendapat bocoran jawaban).
|
||||
const showReview = $derived(shouldShowQuizReview(mgr.quizTerminationReason));
|
||||
|
|
@ -228,6 +241,20 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{#snippet weightedDistCard(dist: WeightedPointDistribution)}
|
||||
<div class="weighted-distribution">
|
||||
<h4 class="weighted-distribution-title">Distribusi Jawaban Berbobot</h4>
|
||||
<div class="weighted-distribution-chips">
|
||||
{#each dist.points as p}
|
||||
<span class="weighted-chip">point {p.weight}: <strong>{p.count}</strong></span>
|
||||
{/each}
|
||||
{#if dist.unanswered > 0}
|
||||
<span class="weighted-chip unanswered">belum dijawab: <strong>{dist.unanswered}</strong></span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="quiz-container" bind:this={quizEl}>
|
||||
{#if showResumeSnackbar}
|
||||
<div class="quiz-snackbar" role="status" aria-live="polite">
|
||||
|
|
@ -328,6 +355,10 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasResultWeightedQuestions}
|
||||
{@render weightedDistCard(resultWeightedDistribution)}
|
||||
{/if}
|
||||
|
||||
{#if result.evalTotal > 0 || result.diagTotal > 0}
|
||||
<div class="score-breakdown">
|
||||
{#if result.evalTotal > 0}
|
||||
|
|
@ -410,6 +441,10 @@
|
|||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if storedAttempt && !result && hasStoredWeightedQuestions}
|
||||
{@render weightedDistCard(storedWeightedDistribution)}
|
||||
{/if}
|
||||
|
||||
{#if storedAttempt && !result && shouldShowQuizReview((storedAttempt.termination_reason ?? null) as QuizTerminationReason | null) && reviewQuestions.length > 0}
|
||||
<div class="summary-review">
|
||||
<h3 class="review-title">Pembahasan (tersimpan)</h3>
|
||||
|
|
@ -715,6 +750,12 @@
|
|||
.score-breakdown { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin: 0.75rem 0 1.25rem; }
|
||||
.breakdown-item { font-size: 0.95rem; padding: 0.35rem 0.7rem; border-radius: 999px; background: var(--color-bg-secondary); border: 1px solid var(--color-border); }
|
||||
.breakdown-item strong { color: var(--color-primary); }
|
||||
.weighted-distribution { margin: 0.75rem 0 1.25rem; text-align: center; }
|
||||
.weighted-distribution-title { font-size: 0.95rem; margin: 0 0 0.5rem; color: var(--color-text); }
|
||||
.weighted-distribution-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; }
|
||||
.weighted-chip { font-size: 0.9rem; padding: 0.3rem 0.7rem; border-radius: 999px; background: rgba(51, 154, 240, 0.1); border: 1px solid rgba(51, 154, 240, 0.35); color: var(--color-text); }
|
||||
.weighted-chip strong { color: var(--color-primary); }
|
||||
.weighted-chip.unanswered { background: var(--color-bg-secondary); border-color: var(--color-border); color: var(--color-text-muted); }
|
||||
.breakdown-item.unmastered { flex-basis: 100%; justify-content: center; }
|
||||
.unmastered-topic { font-size: 0.8rem; background: rgba(220, 53, 69, 0.1); color: var(--color-danger); padding: 0.15rem 0.5rem; border-radius: 6px; margin: 0.15rem; display: inline-block; }
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
quiz_mcq: string;
|
||||
/** Poin soal weighted ("7/10") — kosong bila tidak ada soal weighted. */
|
||||
quiz_weighted: string;
|
||||
/** Distribusi bobot terpilih (JSON) — kosong bila tidak ada soal weighted. */
|
||||
weighted_points: string;
|
||||
}
|
||||
|
||||
interface LessonNode {
|
||||
|
|
@ -78,6 +80,39 @@
|
|||
return Math.round((earned / total) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `_weighted_points` JSON → teks ringkas distribusi bobot.
|
||||
* Contoh: "point 10: 5 · point 7: 2 · belum dijawab: 1".
|
||||
*/
|
||||
function weightedDistributionText(raw: string | number | boolean | undefined): string | null {
|
||||
if (typeof raw !== 'string' || !raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
points?: Array<{ weight?: number; count?: number }>;
|
||||
unanswered?: number;
|
||||
};
|
||||
const points = Array.isArray(parsed.points) ? parsed.points : [];
|
||||
const parts = points
|
||||
.filter((p) => typeof p.weight === 'number' && typeof p.count === 'number')
|
||||
.map((p) => `point ${p.weight}: ${p.count}`);
|
||||
const unanswered = Number(parsed.unanswered ?? 0);
|
||||
if (unanswered > 0) parts.push(`belum dijawab: ${unanswered}`);
|
||||
return parts.length > 0 ? parts.join(' · ') : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip badge "Bobot" — jelaskan rasio sekaligus distribusi bobot
|
||||
* terpilih (counting) dari Task E.
|
||||
*/
|
||||
function weightedBadgeTooltip(s: StudentLessonDetail): string {
|
||||
const base = 'Poin soal QUIZ_WEIGHTED (bobot tertinggi tiap soal dijumlahkan)';
|
||||
const dist = weightedDistributionText(s.weighted_points);
|
||||
return dist ? `${base} — Distribusi: ${dist}` : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tooltip badge skor kuis — jelaskan model skor rata-rata per jenis:
|
||||
* "Rata-rata jenis — MCQ 75% · Weighted 70%".
|
||||
|
|
@ -307,7 +342,8 @@
|
|||
diag_score: String(s[key + '_diag'] ?? ''),
|
||||
diag_unmastered: parseUnmastered(s[key + '_diag_unmastered']),
|
||||
quiz_mcq: String(s[key + '_quiz_mcq'] ?? ''),
|
||||
quiz_weighted: String(s[key + '_quiz_weighted'] ?? '')
|
||||
quiz_weighted: String(s[key + '_quiz_weighted'] ?? ''),
|
||||
weighted_points: String(s[key + '_weighted_points'] ?? '')
|
||||
});
|
||||
|
||||
if (s.role === 'student') {
|
||||
|
|
@ -372,7 +408,8 @@
|
|||
diag_score: String(s[key + '_diag'] ?? ''),
|
||||
diag_unmastered: parseUnmastered(s[key + '_diag_unmastered']),
|
||||
quiz_mcq: String(s[key + '_quiz_mcq'] ?? ''),
|
||||
quiz_weighted: String(s[key + '_quiz_weighted'] ?? '')
|
||||
quiz_weighted: String(s[key + '_quiz_weighted'] ?? ''),
|
||||
weighted_points: String(s[key + '_weighted_points'] ?? '')
|
||||
});
|
||||
|
||||
if (s.role === 'student') {
|
||||
|
|
@ -598,7 +635,7 @@
|
|||
{/if}
|
||||
|
||||
{#if student.quiz_weighted}
|
||||
<span class="badge badge-detail" title="Poin soal QUIZ_WEIGHTED (bobot tertinggi tiap soal dijumlahkan)">Bobot: {student.quiz_weighted}</span>
|
||||
<span class="badge badge-detail" title={weightedBadgeTooltip(student)}>Bobot: {student.quiz_weighted}</span>
|
||||
{/if}
|
||||
|
||||
{#if student.has_violation}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Student progress tracking and reporting JSON API routes.
|
|||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
|
|
@ -30,6 +31,31 @@ def _masked(token: str) -> str:
|
|||
return f"{token[:6]}..." if token else "(kosong)"
|
||||
|
||||
|
||||
def _weighted_points_compact(raw) -> str:
|
||||
"""JSON `_weighted_points` → compact CSV `10:5|7:2|0:1`.
|
||||
|
||||
Kosong bila kuis tidak punya soal weighted (atau payload lama).
|
||||
"""
|
||||
if not isinstance(raw, str) or not raw:
|
||||
return ""
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return ""
|
||||
points = data.get("points") if isinstance(data, dict) else None
|
||||
if not isinstance(points, list):
|
||||
return ""
|
||||
parts = []
|
||||
for p in points:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
try:
|
||||
parts.append(f"{int(p['weight'])}:{int(p['count'])}")
|
||||
except (KeyError, TypeError, ValueError):
|
||||
continue
|
||||
return "|".join(parts)
|
||||
|
||||
|
||||
@progress_bp.route('/reset-progress', methods=['POST'])
|
||||
def reset_progress():
|
||||
"""Reset progress siswa via student_id (Teacher only).
|
||||
|
|
@ -185,7 +211,12 @@ def export_progress_csv():
|
|||
# poin weighted. Kolom kosong bila jenis itu tidak ada di kuis lesson.
|
||||
fieldnames = ['nama_siswa']
|
||||
for slug in lesson_slugs:
|
||||
fieldnames.extend([slug, f"{slug}_quiz_mcq", f"{slug}_quiz_weighted"])
|
||||
fieldnames.extend([
|
||||
slug,
|
||||
f"{slug}_quiz_mcq",
|
||||
f"{slug}_quiz_weighted",
|
||||
f"{slug}_weighted_points",
|
||||
])
|
||||
fieldnames.append('completed_count')
|
||||
|
||||
output = io.StringIO()
|
||||
|
|
@ -207,6 +238,9 @@ def export_progress_csv():
|
|||
row[slug] = ""
|
||||
row[f"{slug}_quiz_mcq"] = student.get(f"{slug}_quiz_mcq", "")
|
||||
row[f"{slug}_quiz_weighted"] = student.get(f"{slug}_quiz_weighted", "")
|
||||
row[f"{slug}_weighted_points"] = _weighted_points_compact(
|
||||
student.get(f"{slug}_weighted_points", "")
|
||||
)
|
||||
row['completed_count'] = student.get('completed_count', 0)
|
||||
writer.writerow(row)
|
||||
|
||||
|
|
|
|||
|
|
@ -309,6 +309,8 @@ def get_all_students_progress(all_lessons_func):
|
|||
mcq_correct = 0
|
||||
weighted_earned = 0
|
||||
weighted_max = 0
|
||||
weighted_points = {}
|
||||
weighted_unanswered = 0
|
||||
|
||||
for ans in answers:
|
||||
qt = _answer_quiz_type(ans)
|
||||
|
|
@ -322,6 +324,16 @@ def get_all_students_progress(all_lessons_func):
|
|||
weighted_earned += int(float(ans.get('earned_points') or 0))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
# Rekap counting bobot terpilih: unanswered dipisah
|
||||
# agar tidak tercampur dengan pilihan berbobot 0.
|
||||
if ans.get('selected_option_id') in (None, ''):
|
||||
weighted_unanswered += 1
|
||||
else:
|
||||
try:
|
||||
pts = int(float(ans.get('earned_points') or 0))
|
||||
except (TypeError, ValueError):
|
||||
pts = 0
|
||||
weighted_points[pts] = weighted_points.get(pts, 0) + 1
|
||||
continue
|
||||
mcq_total += 1
|
||||
if ans.get('is_correct'):
|
||||
|
|
@ -356,12 +368,26 @@ def get_all_students_progress(all_lessons_func):
|
|||
student[f"{lesson.slug}_quiz_weighted"] = (
|
||||
f"{weighted_earned}/{weighted_max}" if weighted_max else ""
|
||||
)
|
||||
# Distribusi bobot (counting) — kosong bila kuis tidak punya
|
||||
# soal weighted. JSON agar UI bisa parse tanpa konvensi baru.
|
||||
student[f"{lesson.slug}_weighted_points"] = (
|
||||
json.dumps({
|
||||
"points": [
|
||||
{"weight": w, "count": c}
|
||||
for w, c in sorted(weighted_points.items(), reverse=True)
|
||||
],
|
||||
"unanswered": weighted_unanswered,
|
||||
})
|
||||
if weighted_max
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
student[f"{lesson.slug}_eval"] = ""
|
||||
student[f"{lesson.slug}_diag"] = ""
|
||||
student[f"{lesson.slug}_diag_unmastered"] = "[]"
|
||||
student[f"{lesson.slug}_quiz_mcq"] = ""
|
||||
student[f"{lesson.slug}_quiz_weighted"] = ""
|
||||
student[f"{lesson.slug}_weighted_points"] = ""
|
||||
# Field komposit (skor gabungan) untuk report guru — emit TERPISAH,
|
||||
# tidak mengubah kontrak status lama di student[lesson.slug].
|
||||
progress_row = rows.get(lesson.id)
|
||||
|
|
|
|||
|
|
@ -335,3 +335,70 @@ def test_export_progress_csv_contains_composite_scores(client):
|
|||
assert siti_row["completed_count"] == "0"
|
||||
|
||||
|
||||
|
||||
def test_report_weighted_points_distribution(client):
|
||||
"""Rekap counting bobot: unanswered dipisah, pilihan bobot 0 tetap tercatat."""
|
||||
import json
|
||||
|
||||
answers = [
|
||||
{"question_id": "q1", "selected_option_id": "o-10", "quiz_type": "weighted",
|
||||
"earned_points": 10, "question_max_points": 10, "is_correct": True},
|
||||
{"question_id": "q2", "selected_option_id": "o-7", "quiz_type": "weighted",
|
||||
"earned_points": 7, "question_max_points": 10, "is_correct": False},
|
||||
{"question_id": "q3", "selected_option_id": "o-0", "quiz_type": "weighted",
|
||||
"earned_points": 0, "question_max_points": 10, "is_correct": False},
|
||||
{"question_id": "q4", "selected_option_id": None, "quiz_type": "weighted",
|
||||
"earned_points": 0, "question_max_points": 10, "is_correct": False},
|
||||
]
|
||||
assert _post_quiz(client, score="17/40", answers=answers).status_code == 200
|
||||
|
||||
resp = client.get(f"/progress-report.json?token={TEACHER_TOKEN}")
|
||||
assert resp.status_code == 200
|
||||
budi = next(s for s in resp.get_json()["students"] if s["nama_siswa"] == "Budi Santoso")
|
||||
|
||||
data = json.loads(budi["hello_world_weighted_points"])
|
||||
assert data["points"] == [
|
||||
{"weight": 10, "count": 1},
|
||||
{"weight": 7, "count": 1},
|
||||
{"weight": 0, "count": 1},
|
||||
]
|
||||
assert data["unanswered"] == 1
|
||||
|
||||
|
||||
def test_export_csv_weighted_points_column(client):
|
||||
"""Kolom CSV compact `10:1|7:1|0:1` (unanswered tidak masuk kolom poin)."""
|
||||
import csv
|
||||
import io
|
||||
|
||||
answers = [
|
||||
{"question_id": "q1", "selected_option_id": "o-10", "quiz_type": "weighted",
|
||||
"earned_points": 10, "question_max_points": 10, "is_correct": True},
|
||||
{"question_id": "q2", "selected_option_id": "o-7", "quiz_type": "weighted",
|
||||
"earned_points": 7, "question_max_points": 10, "is_correct": False},
|
||||
{"question_id": "q3", "selected_option_id": "o-0", "quiz_type": "weighted",
|
||||
"earned_points": 0, "question_max_points": 10, "is_correct": False},
|
||||
]
|
||||
assert _post_quiz(client, score="17/30", answers=answers).status_code == 200
|
||||
|
||||
resp = client.get(f"/progress-report/export-csv?token={TEACHER_TOKEN}")
|
||||
assert resp.status_code == 200
|
||||
reader = csv.DictReader(io.StringIO(resp.data.decode("utf-8-sig")), delimiter=";")
|
||||
assert "hello_world_weighted_points" in reader.fieldnames
|
||||
budi_row = next(r for r in reader if r["nama_siswa"] == "Budi Santoso")
|
||||
assert budi_row["hello_world_weighted_points"] == "10:1|7:1|0:1"
|
||||
|
||||
|
||||
def test_report_legacy_answers_without_quiz_type(client):
|
||||
"""Attempt lama tanpa quiz_type tetap terbaca sebagai MCQ (tanpa weighted)."""
|
||||
answers = [
|
||||
{"question_id": "q1", "selected_option_id": "o1", "is_correct": True, "type": "mcq"},
|
||||
{"question_id": "q2", "selected_option_id": "o2", "is_correct": False, "type": "mcq"},
|
||||
]
|
||||
assert _post_quiz(client, score="1/2", answers=answers).status_code == 200
|
||||
|
||||
resp = client.get(f"/progress-report.json?token={TEACHER_TOKEN}")
|
||||
assert resp.status_code == 200
|
||||
budi = next(s for s in resp.get_json()["students"] if s["nama_siswa"] == "Budi Santoso")
|
||||
assert budi["hello_world_quiz_mcq"] == "1/2"
|
||||
assert budi["hello_world_quiz_weighted"] == ""
|
||||
assert budi["hello_world_weighted_points"] == ""
|
||||
|
|
|
|||
Loading…
Reference in New Issue