import datetime import re import pytest from conftest import outcome_actions TODAY = datetime.date.today().isoformat() @pytest.fixture def instructor(dev_client): dev_client.post("/dev/launch/dev-instructor") return dev_client def test_home_page(instructor): page = instructor.get("/instructor/").get_data(as_text=True) assert "8 active students" in page assert "Live cold call" in page def test_live_flow_records_outcome(instructor): resp = instructor.post( "/instructor/live/next", data={"date": TODAY}, follow_redirects=True ) page = resp.get_data(as_text=True) match = re.search(r'/instructor/call/(\d+)/outcome', page) assert match, "live page should show outcome buttons for the call" assert "GOOD" in page call_id = match.group(1) resp = instructor.post( f"/instructor/call/{call_id}/outcome", data={"action": outcome_actions(page)["GOOD"]}, follow_redirects=True, ) page = resp.get_data(as_text=True) assert "Calls so far today" in page assert "GOOD" in page # The call is resolved, so the next-student button is back. assert "Call next student" in page def test_live_requires_resolution_before_next(instructor): instructor.post("/instructor/live/next", data={"date": TODAY}) page = instructor.get(f"/instructor/live?date={TODAY}").get_data(as_text=True) first_call = re.search(r'/instructor/call/(\d+)/outcome', page).group(1) # Pressing next again does not create a second pending call. instructor.post("/instructor/live/next", data={"date": TODAY}) page = instructor.get(f"/instructor/live?date={TODAY}").get_data(as_text=True) assert re.findall(r'/instructor/call/(\d+)/outcome', page) == [first_call] def test_generate_and_print(instructor): resp = instructor.post( f"/instructor/day/{TODAY}/generate", data={"n": "12"}, follow_redirects=True, ) page = resp.get_data(as_text=True) assert page.count("") == 13 # header + 12 rows def test_regenerate_replaces_pending(instructor): instructor.post(f"/instructor/day/{TODAY}/generate", data={"n": "12"}) resp = instructor.post( f"/instructor/day/{TODAY}/generate", data={"n": "5"}, follow_redirects=True, ) assert resp.get_data(as_text=True).count("") == 6 def test_day_edit_saves_outcomes(instructor): instructor.post(f"/instructor/day/{TODAY}/generate", data={"n": "2"}) page = instructor.get(f"/instructor/day/{TODAY}").get_data(as_text=True) ids = re.findall(r'name="status-(\d+)"', page) assert len(ids) == 2 poor_id = re.search(r'value="(\d+)"[^>]*>POOR', page).group(1) resp = instructor.post( f"/instructor/day/{TODAY}", data={ f"status-{ids[0]}": "answered", f"assessment-{ids[0]}": poor_id, f"note-{ids[0]}": "rough day", f"status-{ids[1]}": "pending", f"delete-{ids[1]}": "on", }, follow_redirects=True, ) page = resp.get_data(as_text=True) assert 'value="rough day"' in page assert len(re.findall(r'name="status-(\d+)"', page)) == 1 def test_outcome_rejects_bad_action_and_foreign_call(instructor): instructor.post("/instructor/live/next", data={"date": TODAY}) page = instructor.get(f"/instructor/live?date={TODAY}").get_data(as_text=True) call_id = re.search(r'/instructor/call/(\d+)/outcome', page).group(1) resp = instructor.post( f"/instructor/call/{call_id}/outcome", data={"action": "nonsense"} ) assert resp.status_code == 400 resp = instructor.post( "/instructor/call/99999/outcome", data={"action": "missing"} ) assert resp.status_code == 404 def test_assessment_scale_editing(instructor): page = instructor.get("/instructor/settings").get_data(as_text=True) ids = { label: lid for lid, label in re.findall( r'name="label-(\d+)" value="([^"]+)"', page ) } assert set(ids) == {"GOOD", "SATISFACTORY", "POOR", "NO MEANINGFUL ANSWER"} # Record a call assessed GOOD so that level is in use. record = instructor.post( "/instructor/live/next", data={"date": TODAY}, follow_redirects=True ).get_data(as_text=True) call_id = re.search(r"/instructor/call/(\d+)/outcome", record).group(1) instructor.post( f"/instructor/call/{call_id}/outcome", data={"action": f"level-{ids['GOOD']}"}, ) # Rename and re-point the in-use level; delete an unused one; add one. form = {"selection_mode": "weighted", "weight_factor": "2", "show_assessments": "on", "new_label": "HEROIC", "new_points": "110"} for label, lid in ids.items(): form[f"label-{lid}"] = "EXCELLENT" if label == "GOOD" else label form[f"points-{lid}"] = "90" if label == "GOOD" else "50" form[f"position-{lid}"] = "0" form[f"delete-{ids['POOR']}"] = "on" form[f"delete-{ids['GOOD']}"] = "on" # in use: must survive page = instructor.post( "/instructor/settings", data=form, follow_redirects=True ).get_data(as_text=True) assert 'value="EXCELLENT"' in page # renamed assert 'value="90.0"' in page # re-pointed assert "POOR" not in page # unused level deleted assert re.search(r"in use \(1\s+call\)", page) # used level kept assert "HEROIC" not in page # >100 points rejected # The rename follows through to recorded calls. day_page = instructor.get(f"/instructor/day/{TODAY}").get_data(as_text=True) assert "EXCELLENT" in day_page def test_settings_roundtrip(instructor): resp = instructor.post( "/instructor/settings", data={"selection_mode": "cycle", "weight_factor": "3.0"}, follow_redirects=True, ) page = resp.get_data(as_text=True) assert '