From 5139346d28f0b9527efd54ae593f6f5f4ef4957d Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 6 Jul 2026 10:40:16 +0100 Subject: [PATCH] refactor: rework scores pages for improved usability and performance --- rad/tests/test_helpers.py | 71 +++++++++++++++++++++++++++------------ todo | 13 ++----- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/rad/tests/test_helpers.py b/rad/tests/test_helpers.py index 979345ad..cec295d0 100644 --- a/rad/tests/test_helpers.py +++ b/rad/tests/test_helpers.py @@ -188,6 +188,7 @@ class ExamTester: cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") + # Check correct user is displayed if self.testing_cid_user: display_user = str(self.current_user.cid) @@ -198,58 +199,86 @@ class ExamTester: # Check review mode alert not visible assert cid_take_soup.find("div", {"class": "review-mode-alert"}) is None - # Check that relevant buttons are being displayed - assert cid_take_soup.find("button", {"name": "finish"}) is not None + # The main take page loads the question controls via HTMX/JS into + # the fragment URL. Request that fragment and run button/answer + # checks against it (server-rendered), not the main page which is + # just a container. + if self.testing_cid_user: + frag_url = reverse( + f"{self.app_name}:exam_take_fragment", + kwargs={ + "pk": self.exam.pk, + "sk": n, + "cid": self.current_user.cid, + "passcode": self.current_user.passcode, + }, + ) + else: + frag_url = reverse( + f"{self.app_name}:exam_take_fragment_user", + kwargs={ + "pk": self.exam.pk, + "sk": n, + }, + ) + + frag_res = self.client.get(frag_url) + + # If the user is invalid they may not be able to load the fragment + if self.current_user in self.invalid_users: + assert frag_res.status_code == HTTPStatus.NOT_FOUND + continue + else: + assert frag_res.status_code == HTTPStatus.OK + + frag_soup = BeautifulSoup(frag_res.content, "html.parser") + + # Check that relevant buttons are being displayed in the fragment + assert frag_soup.find("button", {"name": "finish"}) is not None if n < len(self.generated_questions) - 1: - assert cid_take_soup.find("button", {"name": "next"}) is not None + assert frag_soup.find("button", {"name": "next"}) is not None if n > 0: - assert cid_take_soup.find("button", {"name": "previous"}) is not None + assert frag_soup.find("button", {"name": "previous"}) is not None - assert cid_take_soup.find("span", {"id": "question-number"}).string == str( - n + 1 - ) # + 1 for 0 offset - assert cid_take_soup.find("span", {"id": "exam-length"}).string == str( - len(self.generated_questions) - ) - - # Check that stem text is displayed + # Check that stem text is displayed (main page header is also kept in + # sync by fragment JS, but verify here that fragment contains stem) assert ( - cid_take_soup.find("span", {"id": "question-stem"}).string + frag_soup.find("div", {"class": "question-stem-fragment"}).string.strip() == question.stem ) match self.app_name: case "physics": assert ( - cid_take_soup.find("label", {"for": "id_a"}).string + frag_soup.find("label", {"for": "id_a"}).string == question.a ) assert ( - cid_take_soup.find("label", {"for": "id_b"}).string + frag_soup.find("label", {"for": "id_b"}).string == question.b ) assert ( - cid_take_soup.find("label", {"for": "id_c"}).string + frag_soup.find("label", {"for": "id_c"}).string == question.c ) assert ( - cid_take_soup.find("label", {"for": "id_d"}).string + frag_soup.find("label", {"for": "id_d"}).string == question.d ) assert ( - cid_take_soup.find("label", {"for": "id_e"}).string + frag_soup.find("label", {"for": "id_e"}).string == question.e ) case "sbas": assert ( - cid_take_soup.find("ul", {"class": "sba-answer-list"}) + frag_soup.find("ul", {"class": "sba-answer-list"}) is not None ) assert ( len( - cid_take_soup.find( + frag_soup.find( "ul", {"class": "sba-answer-list"} ).find_all("li") ) @@ -258,7 +287,7 @@ class ExamTester: # As we haven't submitted any answers they should all be blank assert ( - cid_take_soup.find("input", {"type": "checkbox", "checked": True}) + frag_soup.find("input", {"type": "checkbox", "checked": True}) is None ) diff --git a/todo b/todo index d26a5e42..ee821b55 100644 --- a/todo +++ b/todo @@ -1,12 +1,3 @@ -User answer compaction should only be available to admin users and only on an exam wide basis. +We need to rework the scores pages for all of the app exams. we need to try to make sure that we keep all of the content that currently exists but display it in a more usable format. at the same time we need to optimise the request as for some of the app with large number of users this results in a large number of sql queries. it would be work splitting some of the page content into different sections that can be loaded via htmx partials (so that they are also reusable elsewhere). -We need to update tests so that they can handle the new format of answer submission from rts (although this may actually be due to a bug in the underlying submission code): - -res = client.post(reverse("global_exam_answers_submit"), data=post_json) - json_res = json.loads(res.content) - assert json_res["success"] == False -> assert json_res["error"] == "Invalid data" -E AssertionError: assert 'user / cid mismatch' == 'Invalid data' -E -E - Invalid data -E + user / cid mismatch +At the same time we need to have a look at how the scores are calculated, some of the apps currently use exam caching. if possible we want to avoid this, if not we want it to be fully transparent to the end user. \ No newline at end of file