github-actions[bot] commited on
Commit
6549dce
·
1 Parent(s): ade3f94

deploy: backend from c237ff2

Browse files
main.py CHANGED
@@ -6774,6 +6774,7 @@ async def commit_student_account_import(
6774
  }
6775
  if not existing_profile_doc:
6776
  profile_payload["createdAt"] = FIRESTORE_SERVER_TIMESTAMP
 
6777
 
6778
  client.collection("users").document(target_uid).set(profile_payload, merge=True)
6779
 
@@ -7691,6 +7692,7 @@ async def create_student_account_for_teacher(
7691
  "atRiskSubjects": [],
7692
  "hasTakenDiagnostic": False,
7693
  "adviserTeacherId": adviser_teacher_id,
 
7694
  "createdAt": FIRESTORE_SERVER_TIMESTAMP,
7695
  "updatedAt": FIRESTORE_SERVER_TIMESTAMP,
7696
  }
@@ -11757,16 +11759,17 @@ def _validate_quiz_questions(
11757
 
11758
  if options:
11759
  options = [to_plain_text(o, single_line=True, strip_option_label=True) for o in options]
 
11760
  validated.append(QuizQuestion(
11761
  questionType=question_type,
11762
  question=clean_llm_text(q.get("question", "")),
11763
- correctAnswer=to_plain_text(q.get("correctAnswer", ""), single_line=True, strip_option_label=True),
11764
  options=options,
11765
  bloomLevel=bloom_level,
11766
  difficulty=difficulty,
11767
  topic=question_topic,
11768
  points=q.get("points", points_map.get(difficulty, 3)),
11769
- explanation=clean_llm_text(q.get("explanation", "")) or "No explanation provided.",
11770
  provenance=question_provenance,
11771
  ))
11772
 
 
6774
  }
6775
  if not existing_profile_doc:
6776
  profile_payload["createdAt"] = FIRESTORE_SERVER_TIMESTAMP
6777
+ profile_payload["guideOfferPending"] = True
6778
 
6779
  client.collection("users").document(target_uid).set(profile_payload, merge=True)
6780
 
 
7692
  "atRiskSubjects": [],
7693
  "hasTakenDiagnostic": False,
7694
  "adviserTeacherId": adviser_teacher_id,
7695
+ "guideOfferPending": True,
7696
  "createdAt": FIRESTORE_SERVER_TIMESTAMP,
7697
  "updatedAt": FIRESTORE_SERVER_TIMESTAMP,
7698
  }
 
11759
 
11760
  if options:
11761
  options = [to_plain_text(o, single_line=True, strip_option_label=True) for o in options]
11762
+ correct_answer = to_plain_text(q.get("correctAnswer", ""), single_line=True, strip_option_label=True)
11763
  validated.append(QuizQuestion(
11764
  questionType=question_type,
11765
  question=clean_llm_text(q.get("question", "")),
11766
+ correctAnswer=correct_answer,
11767
  options=options,
11768
  bloomLevel=bloom_level,
11769
  difficulty=difficulty,
11770
  topic=question_topic,
11771
  points=q.get("points", points_map.get(difficulty, 3)),
11772
+ explanation=clean_llm_text(q.get("explanation", "")) or f"Correct answer: {correct_answer}",
11773
  provenance=question_provenance,
11774
  ))
11775
 
routes/practice.py CHANGED
@@ -250,7 +250,7 @@ async def generate_practice(request: Request, body: PracticeGenerateRequest):
250
  question=q.get("question", ""),
251
  options=q.get("options", ["", "", "", ""]),
252
  correct_index=correct_idx,
253
- explanation=q.get("explanation", "No explanation available."),
254
  competency=q.get("competency", body.competency),
255
  difficulty=q.get("difficulty", body.difficulty),
256
  bloomsLevel=q.get("bloomsLevel", "Apply"),
 
250
  question=q.get("question", ""),
251
  options=q.get("options", ["", "", "", ""]),
252
  correct_index=correct_idx,
253
+ explanation=q.get("explanation") or "",
254
  competency=q.get("competency", body.competency),
255
  difficulty=q.get("difficulty", body.difficulty),
256
  bloomsLevel=q.get("bloomsLevel", "Apply"),
services/user_provisioning_service.py CHANGED
@@ -269,6 +269,8 @@ class UserProvisioningService:
269
 
270
  profile_payload = self._build_profile_payload(user_input, role_lower, normalized_status)
271
  profile_payload["createdAt"] = self._firestore_server_timestamp
 
 
272
 
273
  try:
274
  firestore_client.collection("users").document(uid).set(profile_payload, merge=True)
 
269
 
270
  profile_payload = self._build_profile_payload(user_input, role_lower, normalized_status)
271
  profile_payload["createdAt"] = self._firestore_server_timestamp
272
+ # New accounts are asked once whether to play the first-use guide; the app clears this flag.
273
+ profile_payload["guideOfferPending"] = True
274
 
275
  try:
276
  firestore_client.collection("users").document(uid).set(profile_payload, merge=True)
tests/test_api.py CHANGED
@@ -2237,6 +2237,29 @@ class TestStudentAccountProvisioningImport:
2237
  provisioned_profile = next(iter(users_store.values()))
2238
  assert provisioned_profile.get("role") == "student"
2239
  assert provisioned_profile.get("forcePasswordChange") is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2240
 
2241
  @staticmethod
2242
  def _move_firestore() -> _ProvisionFirestoreModule:
 
2237
  provisioned_profile = next(iter(users_store.values()))
2238
  assert provisioned_profile.get("role") == "student"
2239
  assert provisioned_profile.get("forcePasswordChange") is True
2240
+ assert provisioned_profile.get("guideOfferPending") is True
2241
+
2242
+ def test_teacher_created_student_account_is_offered_the_first_use_guide(self):
2243
+ firestore = _ProvisionFirestoreModule({"users": {}, "managedStudents": {}, "classSectionOwnership": {}, "accessAuditLogs": {}})
2244
+
2245
+ with patch.object(main_module, "firebase_firestore", firestore), patch.object(main_module, "_firebase_ready", True), patch.object(main_module.firebase_auth, "verify_id_token", return_value={
2246
+ "uid": "teacher-uid",
2247
+ "email": "teacher@example.com",
2248
+ "role": "teacher",
2249
+ }), patch.object(main_module.firebase_auth, "get_user_by_email", side_effect=Exception("user not found")), patch.object(main_module.firebase_auth, "create_user", return_value=type("AuthUser", (), {"uid": "auth-created-1"})()):
2250
+ response = client.post(
2251
+ "/api/teacher/create-student-account",
2252
+ json={
2253
+ "name": "Ana Cruz",
2254
+ "email": "ana@student.com",
2255
+ "temporary_password": "Secure123!",
2256
+ "section": "STEM-A",
2257
+ "adviser_teacher_id": "teacher-uid",
2258
+ },
2259
+ )
2260
+
2261
+ assert response.status_code == 200
2262
+ assert firestore.client().store["users"]["auth-created-1"]["guideOfferPending"] is True
2263
 
2264
  @staticmethod
2265
  def _move_firestore() -> _ProvisionFirestoreModule:
tests/test_practice_router.py CHANGED
@@ -97,6 +97,29 @@ class TestPracticeGenerate:
97
  assert "generated_at" in data
98
  assert data["questions"][0]["id"] == "q1"
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def test_generate_rejects_mismatched_user(self):
101
  """Request with mismatched auth userId should return 403."""
102
  response = client.post(
 
97
  assert "generated_at" in data
98
  assert data["questions"][0]["id"] == "q1"
99
 
100
+ @patch("routes.practice._get_firestore", return_value=None)
101
+ @patch("routes.practice._call_claude")
102
+ def test_missing_explanation_is_left_empty_instead_of_a_shared_sentence(self, mock_call, _mock_db):
103
+ """An omitted or null explanation must not become the same canned text on every question."""
104
+ options = ["3", "4", "5", "6"]
105
+ mock_call.return_value = json.dumps({
106
+ "questions": [
107
+ {"id": "q1", "question": "2+2?", "options": options, "correct_index": 1, "explanation": "Add 2 and 2."},
108
+ {"id": "q2", "question": "3+3?", "options": options, "correct_index": 3},
109
+ {"id": "q3", "question": "4+4?", "options": options, "correct_index": 3, "explanation": None},
110
+ ]
111
+ })
112
+
113
+ user_id = "test_user_123"
114
+ response = client.post(
115
+ "/api/practice/generate",
116
+ json={"userId": user_id, "subject": "Algebra", "competency": "Linear Equations", "difficulty": "Practice", "count": 3},
117
+ headers=_auth_header(user_id),
118
+ )
119
+
120
+ assert response.status_code == 200
121
+ assert [q["explanation"] for q in response.json()["questions"]] == ["Add 2 and 2.", "", ""]
122
+
123
  def test_generate_rejects_mismatched_user(self):
124
  """Request with mismatched auth userId should return 403."""
125
  response = client.post(
tests/test_quiz_generation_regression.py CHANGED
@@ -4,7 +4,7 @@ import pytest
4
  from fastapi.testclient import TestClient
5
  from pydantic import ValidationError
6
 
7
- from main import app
8
  from routes.quiz_generation_routes import QuizGenerationRequest, _build_quiz_generation_prompt
9
 
10
  pytestmark = pytest.mark.usefixtures("isolated_mock_student_auth")
@@ -49,3 +49,17 @@ def test_quiz_type_fallback_prompt_handles_empty_type_list():
49
  "Algebra", "Math", None, 1, [], "easy", "", grade_level="Grade 11"
50
  )
51
  assert "multiple_choice" in prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  from fastapi.testclient import TestClient
5
  from pydantic import ValidationError
6
 
7
+ from main import _validate_quiz_questions, app
8
  from routes.quiz_generation_routes import QuizGenerationRequest, _build_quiz_generation_prompt
9
 
10
  pytestmark = pytest.mark.usefixtures("isolated_mock_student_auth")
 
49
  "Algebra", "Math", None, 1, [], "easy", "", grade_level="Grade 11"
50
  )
51
  assert "multiple_choice" in prompt
52
+
53
+
54
+ def test_missing_quiz_explanation_falls_back_to_the_questions_own_answer():
55
+ questions = [
56
+ {"questionType": "identification", "question": "Define slope", "correctAnswer": "rise over run", "explanation": "Slope is rise over run."},
57
+ {"questionType": "identification", "question": "Define intercept", "correctAnswer": "where the line crosses an axis"},
58
+ {"questionType": "identification", "question": "Define vertex", "correctAnswer": "the turning point", "explanation": " "},
59
+ ]
60
+ validated = _validate_quiz_questions(questions, [])
61
+ assert [q.explanation for q in validated] == [
62
+ "Slope is rise over run.",
63
+ "Correct answer: where the line crosses an axis",
64
+ "Correct answer: the turning point",
65
+ ]
tests/test_user_provisioning_service.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from services.user_provisioning_service import AdminCreateUserInput, UserProvisioningService
2
  from unittest.mock import MagicMock
3
 
@@ -28,3 +29,38 @@ def test_teacher_profile_does_not_require_or_assign_a_section():
28
  assert profile["role"] == "teacher"
29
  assert "section" not in profile
30
  assert "classSectionId" not in profile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
  from services.user_provisioning_service import AdminCreateUserInput, UserProvisioningService
3
  from unittest.mock import MagicMock
4
 
 
29
  assert profile["role"] == "teacher"
30
  assert "section" not in profile
31
  assert "classSectionId" not in profile
32
+
33
+
34
+ @pytest.mark.parametrize("role", ["Student", "Teacher", "Admin"])
35
+ def test_created_accounts_are_offered_the_first_use_guide(role):
36
+ auth = MagicMock()
37
+ auth.get_user_by_email.side_effect = Exception("No user record found")
38
+ auth.create_user.return_value = MagicMock(uid="new-uid")
39
+ firestore_module = MagicMock()
40
+ users = firestore_module.client.return_value.collection.return_value
41
+ users.where.return_value.limit.return_value.stream.return_value = []
42
+ provisioning = UserProvisioningService(
43
+ firebase_auth_module=auth,
44
+ firestore_module=firestore_module,
45
+ firestore_server_timestamp="server-timestamp",
46
+ email_service=MagicMock(),
47
+ )
48
+
49
+ uid = provisioning.create_user(
50
+ AdminCreateUserInput(
51
+ name="New User",
52
+ email="new.user@example.com",
53
+ password="Secure123!",
54
+ confirm_password="Secure123!",
55
+ role=role,
56
+ status="Active",
57
+ grade="Grade 11",
58
+ section="Section A",
59
+ lrn="123456789012",
60
+ )
61
+ )
62
+
63
+ assert uid == "new-uid"
64
+ users.document.assert_called_with("new-uid")
65
+ (profile,) = users.document.return_value.set.call_args.args
66
+ assert profile["guideOfferPending"] is True