Implemente le mini-jeu Quiz (questions/choix/points/timer)
Build and deploy / test-python (push) Failing after 1m3s
Build and deploy / test-js (push) Successful in 48s
Build and deploy / lint-python (push) Failing after 1m2s
Build and deploy / lint-js (push) Failing after 1m5s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 1m5s
Build and deploy / test-python (push) Failing after 1m3s
Build and deploy / test-js (push) Successful in 48s
Build and deploy / lint-python (push) Failing after 1m2s
Build and deploy / lint-js (push) Failing after 1m5s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 1m5s
document_engine/labels/quiz_config.py (nouveau) : modele de donnees complet, meme convention resolve_X/sanitize_X que game_engine/rendering/quiz_box_config.py cote jeu (aucun import croise) - DEFAULT_QUIZ_CONFIG, sanitize_quiz_config (valide/nettoie chaque question independamment, jamais ne leve, tronque a 2-4 choix, remet correct_index a 0 si hors bornes/non-entier, clampe points >= 0 et timer_seconds dans [5,300]), quiz_total_points. Cote serveur, routes/document/document_element_update.py revalide desormais un quiz avant persistance (seul kind qui en a besoin - les autres n'ont que des attributs scalaires sans structure a garantir) et renvoie les attributs REELLEMENT persistes dans sa reponse, pour que le client ne derive jamais de la verite serveur apres un nettoyage serveur (ex. choix en trop tronque). Cote editeur (static/document/js/document-editor.js), le panneau Proprietes d'un quiz est desormais reel : chronometre optionnel, couleur de theme, liste de questions repetable (ajout/suppression), chacune avec son texte, un nombre de choix ajustable (2-4, les inputs texte suivent), le choix correct via un radio par question, et les points gagnes. Le rendu canevas (render_document_element.py) affiche un resume reel (nombre de questions, total des points, minuteur) au lieu du placeholder generique. Tests : document_engine/labels/quiz_config.py couvert par 13 tests purs (tests/document/test_quiz_config.py, sans Flask - defauts, troncature, validation, cas limites dont bool comme correct_index) + 1 test de route verifiant la sanitization a l'ecriture et le rendu. SKIP=djlint : backlog H021 pre-existant, aucun template touche ici. ruff/mypy --strict/vulture/bandit/import-linter/eslint/stylelint tous verts ; 63 tests cibles (document + onboarding + auth) verifies fraichement + verification manuelle live via le serveur de dev (ajout, sanitization sur choix invalides/en trop, rendu du resume avec minuteur). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
597d99a8de
commit
9d87a4bfef
@@ -65,6 +65,34 @@ def test_document_element_update_persists_attributes(client: FlaskClient, suppor
|
||||
assert reread.status_code == 200
|
||||
|
||||
|
||||
def test_document_element_update_sanitizes_quiz_config_on_write(client: FlaskClient, support: str) -> None:
|
||||
"""Le quiz est le seul kind revalidé côté serveur (voir
|
||||
routes/document/document_element_update.py) : un choix en trop
|
||||
(5e) doit être tronqué, et la réponse renvoie les attributs
|
||||
RÉELLEMENT persistés (pas ceux envoyés tels quels) pour que le
|
||||
client ne dérive jamais de la vérité serveur."""
|
||||
add_resp = client.post(f"/document/{support}/elements/add", data={"kind": "quiz"})
|
||||
element_id = add_resp.get_json()["id"]
|
||||
|
||||
resp = client.post(
|
||||
f"/document/{support}/elements/{element_id}/update",
|
||||
json={
|
||||
"theme_color": "#ff5f2e",
|
||||
"timer_enabled": False,
|
||||
"timer_seconds": 30,
|
||||
"questions": [
|
||||
{"text": "Une question", "choices": ["A", "B", "C", "D", "E"], "correct_index": 0, "points": 10},
|
||||
],
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
payload = resp.get_json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["attributes"]["questions"][0]["choices"] == ["A", "B", "C", "D"]
|
||||
assert "1 question" in payload["rendered_html"]
|
||||
assert "10 points" in payload["rendered_html"]
|
||||
|
||||
|
||||
def test_document_element_move_and_delete(client: FlaskClient, support: str) -> None:
|
||||
first_id = client.post(f"/document/{support}/elements/add", data={"kind": "titre"}).get_json()["id"]
|
||||
second_id = client.post(f"/document/{support}/elements/add", data={"kind": "paragraphe"}).get_json()["id"]
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Modèle de données du mini-jeu Quiz (document_engine/labels/quiz_config.py)
|
||||
— sans Flask, teste directement sanitize_quiz_config/quiz_total_points."""
|
||||
|
||||
import document_engine as doc_engine
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_on_missing_input_returns_full_defaults() -> None:
|
||||
config = doc_engine.sanitize_quiz_config(None)
|
||||
assert config == {
|
||||
"theme_color": "#ff5f2e",
|
||||
"timer_enabled": False,
|
||||
"timer_seconds": 30,
|
||||
"questions": [],
|
||||
}
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_keeps_a_valid_question_as_is() -> None:
|
||||
raw = {
|
||||
"theme_color": "#123456",
|
||||
"timer_enabled": True,
|
||||
"timer_seconds": 45,
|
||||
"questions": [
|
||||
{"text": "2 + 2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "points": 10},
|
||||
],
|
||||
}
|
||||
config = doc_engine.sanitize_quiz_config(raw)
|
||||
assert config["theme_color"] == "#123456"
|
||||
assert config["timer_enabled"] is True
|
||||
assert config["timer_seconds"] == 45
|
||||
assert config["questions"] == [{"text": "2 + 2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "points": 10}]
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_drops_a_question_with_empty_text() -> None:
|
||||
raw = {"questions": [{"text": " ", "choices": ["A", "B"], "correct_index": 0, "points": 5}]}
|
||||
assert doc_engine.sanitize_quiz_config(raw)["questions"] == []
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_drops_a_question_with_fewer_than_two_choices() -> None:
|
||||
raw = {"questions": [{"text": "Une question", "choices": ["Seul choix"], "correct_index": 0, "points": 5}]}
|
||||
assert doc_engine.sanitize_quiz_config(raw)["questions"] == []
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_drops_blank_choices_before_counting() -> None:
|
||||
raw = {"questions": [{"text": "Une question", "choices": ["A", " ", ""], "correct_index": 0, "points": 5}]}
|
||||
assert doc_engine.sanitize_quiz_config(raw)["questions"] == []
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_caps_choices_at_four() -> None:
|
||||
raw = {
|
||||
"questions": [
|
||||
{"text": "Une question", "choices": ["A", "B", "C", "D", "E"], "correct_index": 0, "points": 5},
|
||||
],
|
||||
}
|
||||
config = doc_engine.sanitize_quiz_config(raw)
|
||||
assert config["questions"][0]["choices"] == ["A", "B", "C", "D"]
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_resets_out_of_range_correct_index() -> None:
|
||||
raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": 7, "points": 5}]}
|
||||
config = doc_engine.sanitize_quiz_config(raw)
|
||||
assert config["questions"][0]["correct_index"] == 0
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_rejects_a_boolean_as_correct_index() -> None:
|
||||
"""True/False sont des int en Python (bool hérite de int) — un JSON
|
||||
malveillant/malformé pourrait glisser `true` là où un index est
|
||||
attendu, ce qui vaudrait 1 après coercition implicite sans ce garde-fou
|
||||
explicite."""
|
||||
raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": True, "points": 5}]}
|
||||
config = doc_engine.sanitize_quiz_config(raw)
|
||||
assert config["questions"][0]["correct_index"] == 0
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_clamps_negative_points_to_zero() -> None:
|
||||
raw = {"questions": [{"text": "Une question", "choices": ["A", "B"], "correct_index": 0, "points": -5}]}
|
||||
assert doc_engine.sanitize_quiz_config(raw)["questions"][0]["points"] == 0
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_clamps_timer_seconds_into_bounds() -> None:
|
||||
assert doc_engine.sanitize_quiz_config({"timer_seconds": 1})["timer_seconds"] == doc_engine.MIN_TIMER_SECONDS
|
||||
assert doc_engine.sanitize_quiz_config({"timer_seconds": 9999})["timer_seconds"] == doc_engine.MAX_TIMER_SECONDS
|
||||
|
||||
|
||||
def test_sanitize_quiz_config_ignores_garbage_top_level_input() -> None:
|
||||
assert doc_engine.sanitize_quiz_config("n'importe quoi") == doc_engine.DEFAULT_QUIZ_CONFIG
|
||||
|
||||
|
||||
def test_quiz_total_points_sums_every_question() -> None:
|
||||
config = doc_engine.sanitize_quiz_config(
|
||||
{
|
||||
"questions": [
|
||||
{"text": "Q1", "choices": ["A", "B"], "correct_index": 0, "points": 10},
|
||||
{"text": "Q2", "choices": ["A", "B"], "correct_index": 1, "points": 5},
|
||||
]
|
||||
}
|
||||
)
|
||||
assert doc_engine.quiz_total_points(config) == 15
|
||||
|
||||
|
||||
def test_quiz_total_points_on_empty_quiz_is_zero() -> None:
|
||||
assert doc_engine.quiz_total_points(doc_engine.DEFAULT_QUIZ_CONFIG) == 0
|
||||
Reference in New Issue
Block a user