pdf-quiz-generator/backend/tests/test_option_explanations.py
Daniel 91ec6a501c feat: key points smart links, difficulty tags, adaptive sessions, educator-only question management
Key points on questions link into article sections (AMBOSS-style) with samples; difficulty tagging with builder/bank filters; adaptive session algorithm prefers unanswered questions then recycles older incorrect ones, weakest categories first with damping; question create/edit is now admin/educator only; expired exams no longer auto-submit on resume; exam suspend messaging updated. Migrations k4f5a6b7c819, l5a6b7c8d920, m6a7b8c9d031. 63 backend and 97 frontend tests pass.
2026-09-09 02:26:45 +02:00

103 lines
5.7 KiB
Python

"""Per-option explanation validation and serialization."""
import unittest
import test_quiz_builder as fixtures
from app.models.question import Question
from app.routers import questions, quizzes
class OptionExplanationTests(unittest.TestCase):
def setUp(self):
self.bank = fixtures.BuilderTests()
self.bank.setUp()
self.client = self.bank.client
def tearDown(self):
self.bank.tearDown()
def create_payload(self, **overrides):
payload = {"question_text": "Explained", "question_type": "mcq",
"options": ["yes", "no"], "correct_answer": "yes",
"option_explanations": {"yes": "Right because", "no": "Wrong because"}}
payload.update(overrides)
return payload
def test_create_validates_and_serializes_explanations(self):
self.bank.user = self.bank.mod
created = self.client.post('/questions/create', json=self.create_payload())
self.assertIn(created.status_code, (200, 201), created.text)
question_id = created.json()['id']
bank = self.client.get('/questions/bank', params={'q': 'Explained'}).json()
row = next(r for r in bank['questions'] if r['id'] == question_id)
self.assertEqual(row['option_explanations'], {"yes": "Right because", "no": "Wrong because"})
for bad in (
{"option_explanations": {"nope": "x"}},
{"option_explanations": "not-a-dict"},
{"option_explanations": {"yes": "x" * 2001}},
):
response = self.client.post('/questions/create', json=self.create_payload(**bad))
self.assertIn(response.status_code, (400, 422), bad)
def test_edit_requires_keys_to_match_current_options(self):
self.bank.user = self.bank.mod
response = self.client.patch('/questions/3', json={"option_explanations": {"yes": "Correct path", "no": "Incorrect path"}})
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(self.bank.db.get(Question, 3).option_explanations, {"yes": "Correct path", "no": "Incorrect path"})
# Changing options without updating keys is rejected atomically.
response = self.client.patch('/questions/3', json={"options": ["a", "b"], "correct_answer": "a",
"option_explanations": {"yes": "stale"}})
self.assertEqual(response.status_code, 400, response.text)
self.assertEqual(self.bank.db.get(Question, 3).options, ["yes", "no"])
def test_key_points_validate_serialize_and_smart_link(self):
from app.models.article import Article
article = Article(slug='kp-target', title='Key point target', content='Intro',
sections=[{'id': 'e' * 32, 'slug': 'kp', 'title': 'KP section', 'content': 'Body'}],
user_id=3, status='published')
self.bank.db.add(article)
self.bank.db.commit()
self.bank.user = self.bank.mod
good = [{'text': 'Brief take-away', 'article_id': article.id, 'article_section_id': 'e' * 32}]
created = self.client.post('/questions/create', json=self.create_payload(key_points=good))
self.assertIn(created.status_code, (200, 201), created.text)
question_id = created.json()['id']
bank = self.client.get('/questions/bank', params={'q': 'Explained'}).json()
row = next(r for r in bank['questions'] if r['id'] == question_id)
self.assertEqual(row['key_points'][0]['article_id'], article.id)
for bad in (
[{'text': 'x', 'article_id': 999}],
[{'text': 'x', 'article_id': article.id, 'article_section_id': 'bad'}],
[{'text': ''}],
'not-a-list',
[{'text': 'x'} for _ in range(13)],
):
response = self.client.patch(f'/questions/{question_id}', json={'key_points': bad})
self.assertIn(response.status_code, (400, 422), bad)
response = self.client.patch(f'/questions/{question_id}', json={'key_points': []})
self.assertEqual(response.status_code, 200, response.text)
self.assertIsNone(self.bank.db.get(fixtures.Question, question_id).key_points or None)
# Study reveal carries key points.
self.bank.user = self.bank.mod
self.client.patch('/questions/1', json={'key_points': good})
self.bank.user = self.bank.owner
attempt = self.client.post('/attempts/start?quiz_id=1&mode=study').json()['id']
detail = self.client.get(f'/quizzes/1?attempt_id={attempt}').json()
self.assertEqual(detail['questions'][0]['key_points'][0]['article_section_id'], 'e' * 32)
def test_quiz_reveal_and_editor_route_carry_explanations(self):
self.bank.user = self.bank.mod
self.client.patch('/questions/1', json={"option_explanations": {"yes": "Preferred response", "no": "Distractor"}})
self.bank.user = self.bank.owner
attempt = self.client.post('/attempts/start?quiz_id=1&mode=study').json()['id']
detail = self.client.get(f'/quizzes/1?attempt_id={attempt}').json()
self.assertEqual(detail['questions'][0]['option_explanations'], {"yes": "Preferred response", "no": "Distractor"})
self.bank.user = self.bank.mod
response = self.client.patch('/quizzes/1/questions/1', json={"option_explanations": {"nope": "bad"}})
self.assertEqual(response.status_code, 400, response.text)
response = self.client.patch('/quizzes/1/questions/1', json={"option_explanations": {"no": "Updated distractor"}})
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(self.bank.db.get(Question, 1).option_explanations, {"no": "Updated distractor"})
if __name__ == '__main__':
unittest.main()