From 656f1658f8ab759d34b2d62b3eb6e68fd8822613 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 26 Jun 2023 11:52:23 +0100 Subject: [PATCH] add (correct) support for logged in users to sba and physics exams --- physics/templates/physics/exam_start.html | 2 +- physics/tests/test_physics_exams.py | 302 +++++++++++++--------- physics/urls.py | 6 +- physics/views.py | 25 +- rad/api.py | 4 + rad/test_helpers.py | 4 +- sbas/templates/sbas/exam_start.html | 4 + sbas/urls.py | 5 + sbas/views.py | 122 +++++---- templates/base.html | 1 - 10 files changed, 288 insertions(+), 187 deletions(-) diff --git a/physics/templates/physics/exam_start.html b/physics/templates/physics/exam_start.html index e113b5cc..96be8048 100755 --- a/physics/templates/physics/exam_start.html +++ b/physics/templates/physics/exam_start.html @@ -4,7 +4,7 @@

Start exam: {{exam.name}}

- {% if request.user.is_authenticated %} + {% if request.user.is_authenticated and valid_user %} Start Exam {% else %} Enter your CID and passcode in the below boxes.
diff --git a/physics/tests/test_physics_exams.py b/physics/tests/test_physics_exams.py index d0dcc2c1..dacd3b99 100644 --- a/physics/tests/test_physics_exams.py +++ b/physics/tests/test_physics_exams.py @@ -27,10 +27,6 @@ from django.contrib.auth.models import User APP_NAME = "physics" - - - - def create_exam(db): exam: Exam = PhysicsExamViews.Exam.objects.create( name="Cid Exam", exam_mode=True, active=True @@ -43,7 +39,7 @@ def create_exam(db): exam.valid_cid_users.add(*group1.ciduser_set.all()) - #exam.valid_user_users.add(user1) + # exam.valid_user_users.add(user1) exam.valid_user_users.add(*usergroup1.users.all()) assert exam in group1.physics_cid_user_groups.all() @@ -105,7 +101,11 @@ def test_exams(db, client): create_category(db) - generated_questions = [create_question(exam), create_question(exam), create_question(exam)] + generated_questions = [ + create_question(exam), + create_question(exam), + create_question(exam), + ] active_exams = client.get(reverse(f"{APP_NAME}:active_exams")) assert active_exams.status_code == 200 @@ -116,12 +116,12 @@ def test_exams(db, client): cid_user_2001 = CidUser.objects.get(cid=2001) user1 = User.objects.get(username="user1") - + user2 = User.objects.get(username="user2") users_tested = 1 - for cid_user in [cid_user_1001, cid_user_1000, user1]: - + # for cid_user in [cid_user_1001, cid_user_1000, user1, user2]: + for cid_user in [cid_user_1001, cid_user_1000, user1, user2]: if isinstance(cid_user, CidUser): testing_cid_user = True else: @@ -129,7 +129,6 @@ def test_exams(db, client): current_user = cid_user client.force_login(cid_user) - exam.active = True exam.publish_results = False exam.save() @@ -137,10 +136,15 @@ def test_exams(db, client): reverse(f"physics:exam_start", kwargs={"pk": exam.pk}) ) + print(cid_user) + + # Check user access to the start page + assert start_page_res.status_code == HTTPStatus.OK start_page_soup = BeautifulSoup(start_page_res.content, "html.parser") - if testing_cid_user: + print(start_page_soup) + if testing_cid_user or cid_user in [user2]: assert "Enter your CID and passcode" in str(start_page_soup) - else: + else: assert "Start Exam" in str(start_page_soup) for n, question in enumerate(generated_questions): @@ -163,10 +167,15 @@ def test_exams(db, client): }, ) + cid_take_res = client.get(take_url) + + # Check that users that have not been added cannot access the take pages + if cid_user in [user2]: + assert cid_take_res.status_code == HTTPStatus.NOT_FOUND + continue + else: + assert cid_take_res.status_code == HTTPStatus.OK - cid_take_res = client.get( - take_url - ) cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") # Check correct user is displayed @@ -174,10 +183,7 @@ def test_exams(db, client): display_user = str(cid_user.cid) else: display_user = current_user.username - assert ( - display_user - in cid_take_soup.find("span", {"id": "user-id"}).string - ) + assert display_user in cid_take_soup.find("span", {"id": "user-id"}).string # Check review mode alert not visible assert cid_take_soup.find("div", {"class": "review-mode-alert"}) is None @@ -216,7 +222,6 @@ def test_exams(db, client): is None ) - if testing_cid_user: take_overview_url = reverse( f"physics:exam_take_overview", @@ -234,27 +239,32 @@ def test_exams(db, client): }, ) - + cid_take_overview_res = client.get(take_overview_url) - cid_take_overview_res = client.get( - take_overview_url - ) - cid_take_overview_soup = BeautifulSoup( - cid_take_overview_res.content, "html.parser" - ) - assert ( - "0 out of 3 questions answered" - in cid_take_overview_soup.find( - "div", attrs={"class": "overview-text"} - ).string - ) + # Check that users that have not been added cannot access the take pages + if cid_user in [user2]: + assert cid_take_overview_res.status_code == HTTPStatus.NOT_FOUND + else: + assert cid_take_overview_res.status_code == HTTPStatus.OK - assert ( - len( - cid_take_overview_soup.find_all("button", attrs={"class": "unanswered"}) + cid_take_overview_soup = BeautifulSoup( + cid_take_overview_res.content, "html.parser" + ) + assert ( + "0 out of 3 questions answered" + in cid_take_overview_soup.find( + "div", attrs={"class": "overview-text"} + ).string + ) + + assert ( + len( + cid_take_overview_soup.find_all( + "button", attrs={"class": "unanswered"} + ) + ) + == 3 ) - == 3 - ) # Loop through questions again to test submissions # All physics questions have 5 parts (and require a csrf token) @@ -280,6 +290,13 @@ def test_exams(db, client): ) cid_take_res = client.get(question_url) + + if cid_user in [user2]: + assert cid_take_res.status_code == HTTPStatus.NOT_FOUND + continue + else: + assert cid_take_res.status_code == HTTPStatus.OK + cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") # Extract csrftoken @@ -331,7 +348,6 @@ def test_exams(db, client): assert all(cid_user_answer.get_answers()) - # Recheck the overview page (now that we have submitted answers) if testing_cid_user: take_overview_url = reverse( @@ -350,86 +366,119 @@ def test_exams(db, client): }, ) - cid_take_overview_res = client.get( - take_overview_url - ) - cid_take_overview_soup = BeautifulSoup( - cid_take_overview_res.content, "html.parser" - ) - assert ( - "3 out of 3 questions answered" - in cid_take_overview_soup.find( - "div", attrs={"class": "overview-text"} - ).string - ) + cid_take_overview_res = client.get(take_overview_url) - assert ( - len( - cid_take_overview_soup.find_all("button", attrs={"class": "unanswered"}) - ) - == 0 - ) - - if testing_cid_user: - # Get overview scores page (invalid passcode) - AssertNotFound(client, - reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": "AAAA"}) - ) - - cid_scores_url = reverse(f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode}) + if cid_user in [user2]: + assert cid_take_overview_res.status_code == HTTPStatus.NOT_FOUND else: - cid_scores_url = reverse(f"user_scores") + assert cid_take_overview_res.status_code == HTTPStatus.OK + + cid_take_overview_soup = BeautifulSoup( + cid_take_overview_res.content, "html.parser" + ) + assert ( + "3 out of 3 questions answered" + in cid_take_overview_soup.find( + "div", attrs={"class": "overview-text"} + ).string + ) + + assert ( + len( + cid_take_overview_soup.find_all( + "button", attrs={"class": "unanswered"} + ) + ) + == 0 + ) + + if testing_cid_user: + # Get overview scores page (invalid passcode) + AssertNotFound( + client, + reverse( + f"cid_scores", kwargs={"cid": cid_user.cid, "passcode": "AAAA"} + ), + ) + + cid_scores_url = reverse( + f"cid_scores", + kwargs={"cid": cid_user.cid, "passcode": cid_user.passcode}, + ) + else: + cid_scores_url = reverse(f"user_scores") # Get overview scores page - cid_scores_res = client.get( - cid_scores_url - ) + cid_scores_res = client.get(cid_scores_url) + + assert cid_scores_res.status_code == HTTPStatus.OK cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser") - search_exam = ( - cid_scores_soup.find("div", {"id": "exam-assigned"}) - .find("ul", {"class": exam.app_name.lower()}) - .find_all("li", attrs={"data-exam-id": exam.pk}) - ) - assert search_exam - assert exam.name in str(search_exam) - assert "Active" in str(search_exam) - # assert "Active" in assigned_exams # check it is active + if cid_user in [user2]: + # page should load but we shouldn't be able to find the exam + with pytest.raises(AttributeError): + search_exam = ( + cid_scores_soup.find("div", {"id": "exam-assigned"}) + .find("ul", {"class": exam.app_name.lower()}) + .find_all("li", attrs={"data-exam-id": exam.pk}) + ) + else: + search_exam = ( + cid_scores_soup.find("div", {"id": "exam-assigned"}) + .find("ul", {"class": exam.app_name.lower()}) + .find_all("li", attrs={"data-exam-id": exam.pk}) + ) + assert search_exam + assert exam.name in str(search_exam) + assert "Active" in str(search_exam) + # assert "Active" in assigned_exams # check it is active - invalid_exam = cid_scores_soup.find_all( - "li", attrs={"data-exam-id": exam.pk + 5} - ) - assert not invalid_exam # Check we can't find an invalid exam - - # Check that we have a results link - results_exam = ( - cid_scores_soup.find("div", {"id": "exam-results"}) - .find("ul", {"class": exam.app_name}) - .find_all("li", attrs={"data-exam-id": exam.pk}) - ) - assert results_exam - assert exam.name in str(results_exam) - assert exam.name + " test" not in str(results_exam) - assert "Results Published" not in str( - results_exam - ) # It should not be published yet + invalid_exam = cid_scores_soup.find_all( + "li", attrs={"data-exam-id": exam.pk + 5} + ) + assert not invalid_exam # Check we can't find an invalid exam + # Check that we have a results link + results_exam = ( + cid_scores_soup.find("div", {"id": "exam-results"}) + .find("ul", {"class": exam.app_name}) + .find_all("li", attrs={"data-exam-id": exam.pk}) + ) + assert results_exam + assert exam.name in str(results_exam) + assert exam.name + " test" not in str(results_exam) + assert "Results Published" not in str( + results_exam + ) # It should not be published yet if testing_cid_user: - AssertNotFound(client, + AssertNotFound( + client, reverse( f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": "AAAA"}, - ) + ), ) - AssertNotFound(client, - reverse(f"{exam.app_name}:exam_scores_cid_user", kwargs={"pk": exam.pk, "cid": cid_user_2001.cid, "passcode": cid_user_2001.passcode}) + AssertNotFound( + client, + reverse( + f"{exam.app_name}:exam_scores_cid_user", + kwargs={ + "pk": exam.pk, + "cid": cid_user_2001.cid, + "passcode": cid_user_2001.passcode, + }, + ), ) exam_scores_url = reverse( f"{exam.app_name}:exam_scores_cid_user", - kwargs={"pk": exam.pk, "cid": cid_user.cid, "passcode": cid_user.passcode}, + kwargs={ + "pk": exam.pk, + "cid": cid_user.cid, + "passcode": cid_user.passcode, + }, ) else: exam_scores_url = reverse( @@ -437,11 +486,15 @@ def test_exams(db, client): kwargs={"pk": exam.pk}, ) - # Load the exam results page - cid_exam_scores_res = client.get( - exam_scores_url - ) + cid_exam_scores_res = client.get(exam_scores_url) + + if cid_user in [user2]: + assert cid_exam_scores_res.status_code == HTTPStatus.NOT_FOUND + continue + else: + assert cid_exam_scores_res.status_code == HTTPStatus.OK + cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser") # Check that we have an alert that exams are not published @@ -468,9 +521,7 @@ def test_exams(db, client): generated_questions.append(extra_question) # Load the exam results page (again) - cid_exam_scores_res = client.get( - exam_scores_url - ) + cid_exam_scores_res = client.get(exam_scores_url) cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser") answer_lis = cid_exam_scores_soup.find_all("li", {"class": "question-part"}) @@ -493,9 +544,7 @@ def test_exams(db, client): exam.save() # Load the exam results page (again) - cid_exam_scores_res = client.get( - exam_scores_url - ) + cid_exam_scores_res = client.get(exam_scores_url) cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser") assert ( @@ -541,14 +590,30 @@ def test_exams(db, client): # answer the final question if testing_cid_user: - cid_user_answer = UserAnswer(question=extra_question, a=True, b=False, c=True, d=False, e=True, cid=cid_user.cid, exam=exam) + cid_user_answer = UserAnswer( + question=extra_question, + a=True, + b=False, + c=True, + d=False, + e=True, + cid=cid_user.cid, + exam=exam, + ) else: - cid_user_answer = UserAnswer(question=extra_question, a=True, b=False, c=True, d=False, e=True, user=current_user, exam=exam) + cid_user_answer = UserAnswer( + question=extra_question, + a=True, + b=False, + c=True, + d=False, + e=True, + user=current_user, + exam=exam, + ) cid_user_answer.save() - cid_exam_scores_res = client.get( - exam_scores_url - ) + cid_exam_scores_res = client.get(exam_scores_url) cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser") # As we have answered correctly the new score should be increased by 5 @@ -559,7 +624,6 @@ def test_exams(db, client): == "14" ) - # Check what happens when exam is not active exam.active = False exam.save() @@ -589,11 +653,9 @@ def test_exams(db, client): "sk": 0, }, ) - cid_take_res = client.get( - cid_take_url - ) + cid_take_res = client.get(cid_take_url) cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser") assert "Exam is currently inactive" in cid_take_soup.text generated_questions.pop(-1).delete() - users_tested = users_tested + 1 \ No newline at end of file + users_tested = users_tested + 1 diff --git a/physics/urls.py b/physics/urls.py index 37129187..3a11e6ef 100644 --- a/physics/urls.py +++ b/physics/urls.py @@ -9,13 +9,8 @@ urlpatterns = [] urlpatterns.extend( [ - # path("question/", views.QuestionView.as_view(), name="question_view"), path("question/", views.QuestionView.as_view(), name="question_view"), path("question//", views.question_detail, name="question_detail"), - # path("question/create/", views.QuestionCreate.as_view(), name="anatomy_question_create"), - # path("question//update", views.QuestionUpdate.as_view(), name="anatomy_question_update"), - # path("exam///mark", views.mark, name="mark"), - # path("exam//mark", views.mark_overview, name="mark_overview"), path("exam//take_old", views.exam_take_old, name="exam_take_old"), path( "exam/////take", @@ -64,6 +59,7 @@ urlpatterns.extend( views.UserAnswerDelete.as_view(), name="user_answer_delete", ), + # TODO: consider merging with generic... path( "exam//scores///", views.exam_scores_cid_user, diff --git a/physics/views.py b/physics/views.py index 13d64ac4..a2bef598 100644 --- a/physics/views.py +++ b/physics/views.py @@ -99,13 +99,15 @@ def active_exams(request): active_exams.append(exam) return render(request, "physics/available_exam_list.html", {"exams": active_exams}) + def exam_scores_cid_user(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) if not exam.exam_mode: raise Http404("Packet not in exam mode") - if cid is not None and not exam.check_cid_user(cid, passcode, request): + print(cid, passcode, request.user) + if not exam.check_cid_user(cid, passcode, request): raise Http404("Error accessing exam") questions = exam.exam_questions.all() @@ -168,7 +170,6 @@ def exam_scores_cid_user(request, pk, cid=None, passcode=None): def exam_take_old(request, pk): - exam = get_object_or_404(Exam, pk=pk) if not exam.active: @@ -197,6 +198,9 @@ def exam_start(request, pk): "physics/exam_start.html", { "exam": exam, + # This may be overly comlicated but if the logged in user + # does not have access we give the option to take it as a cid user + "valid_user": exam.check_logged_in_user(request), }, ) @@ -204,12 +208,11 @@ def exam_start(request, pk): def exam_take_overview(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) - if cid is not None and not exam.check_cid_user(cid, passcode): - raise Http404("Error accessing exam") - if not exam.active: return exam_inactive(request, context={"exam": exam}) + if not exam.check_cid_user(cid, passcode, request): + raise Http404("Error accessing exam") questions = exam.exam_questions.all() @@ -253,14 +256,13 @@ def exam_take_overview(request, pk, cid=None, passcode=None): ) -def exam_take(request, pk, sk, cid=None, passcode=None): - +def exam_take(request, pk: int, sk: int, cid: str = None, passcode: str = None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: return exam_inactive(request, context={"exam": exam}) - if cid is not None and not exam.check_cid_user(cid, passcode): + if not exam.check_cid_user(cid, passcode, request): raise Http404("Error accessing exam") if cid is not None: @@ -329,7 +331,6 @@ def exam_take(request, pk, sk, cid=None, passcode=None): else: form = UserAnswerForm(instance=answer) - saved_answer = False if answer is not None: saved_answer = [answer.a, answer.b, answer.c, answer.d, answer.e] @@ -346,6 +347,7 @@ def exam_take(request, pk, sk, cid=None, passcode=None): "previous": previous, "exam_length": exam_length, "pos": pos, + "saved_answer": saved_answer, "passcode": passcode, "cid_user_exam": cid_user_exam, }, @@ -413,6 +415,7 @@ GenericExamViews = ExamViews(Exam, Question, None, UserAnswer, "physics", "physi GenericViews = GenericViewBase("physics", Question, UserAnswer, Exam) + class ExamCreate(ExamCreateBase): model = Exam form_class = ExamForm @@ -427,7 +430,6 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): model = Exam - class ExamClone(AuthorOrCheckerRequiredMixin, ExamCloneMixin, ExamCreate): """Clone exam view""" @@ -439,8 +441,6 @@ class ExamAuthorUpdate( form_class = ExamAuthorForm - - class QuestionView( LoginRequiredMixin, SingleTableMixin, FilterView, AuthorOrCheckerRequiredMixin ): @@ -456,7 +456,6 @@ class QuestionView( return context - class UserAnswerView(AuthorOrCheckerRequiredMixin, LoginRequiredMixin, DetailView): model = UserAnswer diff --git a/rad/api.py b/rad/api.py index e2c3d2f6..ef2dc9fa 100644 --- a/rad/api.py +++ b/rad/api.py @@ -3,6 +3,8 @@ from ninja import NinjaAPI from rapids.api import router as rapids_router from longs.api import router as longs_router from anatomy.api import router as anatomy_router +from sbas.api import router as sbas_router +from physics.api import router as physics_router api = NinjaAPI(csrf=True, version="1") @@ -11,6 +13,8 @@ api = NinjaAPI(csrf=True, version="1") api.add_router("rapid/", rapids_router) api.add_router("anatomy/", anatomy_router) api.add_router("longs/", longs_router) +api.add_router("sbas/", sbas_router) +api.add_router("physics/", physics_router) @api.get("/hello") def hello(request): diff --git a/rad/test_helpers.py b/rad/test_helpers.py index dce6a7a7..ccb29c69 100644 --- a/rad/test_helpers.py +++ b/rad/test_helpers.py @@ -22,6 +22,8 @@ def create_cid_user_and_groups(db): # Create UserUsers user1 = User.objects.create_user(username="user1", email="test@email.com", password="test1234") + user2 = User.objects.create_user(username="user2", email="test2@email.com", password="test1234") usergroup1 = UserUserGroup.objects.create(name="Group1") usergroup1.users.add(user1) - usergroup2 = UserUserGroup.objects.create(name="Group2") \ No newline at end of file + usergroup2 = UserUserGroup.objects.create(name="Group2") + usergroup2.users.add(user2) \ No newline at end of file diff --git a/sbas/templates/sbas/exam_start.html b/sbas/templates/sbas/exam_start.html index 45cb8f5c..634140cc 100755 --- a/sbas/templates/sbas/exam_start.html +++ b/sbas/templates/sbas/exam_start.html @@ -4,6 +4,9 @@

Start exam: {{exam.name}}

+ {% if request.user.is_authenticated and valid_user %} + Start Exam + {% else %} Enter your CID and passcode in the below boxes.

@@ -44,4 +47,5 @@ }); }); + {% endif %} {% endblock %} \ No newline at end of file diff --git a/sbas/urls.py b/sbas/urls.py index 2fb30b1b..3d446317 100644 --- a/sbas/urls.py +++ b/sbas/urls.py @@ -32,6 +32,11 @@ urlpatterns = [ views.exam_take, name="exam_take", ), + path( + "exam///take", + views.exam_take, + name="exam_take_user", + ), path("exam//start", views.exam_start, name="exam_start"), path( "exam////finish", diff --git a/sbas/views.py b/sbas/views.py index 2a5cce07..cd914eab 100644 --- a/sbas/views.py +++ b/sbas/views.py @@ -24,7 +24,7 @@ from django.core.cache import cache from django.urls import reverse_lazy, reverse -from django.http import Http404, JsonResponse +from django.http import Http404, HttpResponseBadRequest, JsonResponse from django.http import HttpResponseRedirect, HttpResponse @@ -41,7 +41,16 @@ import json import statistics import plotly.express as px -from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamCreateBase, ExamDeleteBase, ExamUpdateBase, ExamViews, GenericViewBase, exam_inactive +from generic.views import ( + AuthorRequiredMixin, + ExamCloneMixin, + ExamCreateBase, + ExamDeleteBase, + ExamUpdateBase, + ExamViews, + GenericViewBase, + exam_inactive, +) from rest_framework import viewsets, permissions from rest_framework.pagination import PageNumberPagination @@ -156,29 +165,29 @@ def exam_start(request, pk): if not exam.active: return exam_inactive(request, context={"exam": exam}) - raise Http404("Exam not found") return render( request, "sbas/exam_start.html", - { - "exam": exam, - }, + {"exam": exam, "valid_user": exam.check_logged_in_user(request)}, ) -def exam_take_overview(request, pk, cid, passcode): +def exam_take_overview(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: return exam_inactive(request, context={"exam": exam}) - if not exam.check_cid_user(cid, passcode): + if not exam.check_cid_user(cid, passcode, request): raise Http404("Error accessing exam") questions = exam.exam_questions.all() - answers = UserAnswer.objects.filter(cid=cid, exam=exam) + if cid is not None: + answers = UserAnswer.objects.filter(cid=cid, exam=exam) + else: + answers = UserAnswer.objects.filter(user=request.user, exam=exam) answer_question_map = {} for ans in answers: @@ -193,8 +202,11 @@ def exam_take_overview(request, pk, cid, passcode): else: question_answer_tuples.append((q, None)) - c = CidUser.objects.filter(cid=cid).first() - cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) + if cid is not None: + c = CidUser.objects.filter(cid=cid).first() + cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) + else: + cid_user_exam = exam.get_or_create_cid_user_exam(user_user=request.user) return render( request, @@ -211,18 +223,20 @@ def exam_take_overview(request, pk, cid, passcode): ) -def exam_take(request, pk, sk, cid, passcode): - +def exam_take(request, pk: int, sk: int, cid: str = None, passcode: str = None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: raise Http404("Exam not found") - if not exam.check_cid_user(cid, passcode): + if not exam.check_cid_user(cid, passcode, request): raise Http404("Error accessing exam") - c = CidUser.objects.filter(cid=cid).first() - cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) + if cid is not None: + c = CidUser.objects.filter(cid=cid).first() + cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c) + else: + cid_user_exam = exam.get_or_create_cid_user_exam(user_user=request.user) question = exam.exam_questions.all()[sk] @@ -230,7 +244,17 @@ def exam_take(request, pk, sk, cid, passcode): pos = exam.get_question_index(question) - answer = question.cid_user_answers.filter(cid=cid, exam=exam).first() + if cid is not None: + answer = question.cid_user_answers.filter(cid=cid, exam=exam).first() + else: + answer = question.cid_user_answers.filter(user=request.user, exam=exam).first() + + previous = -1 + if sk > 0: + previous = sk - 1 + next = sk + 1 + if sk == exam_length - 1: + next = False if request.method == "POST": if answer: @@ -239,7 +263,11 @@ def exam_take(request, pk, sk, cid, passcode): form = UserAnswerForm(request.POST) if form.is_valid(): answer = form.save(commit=False) - answer.cid = cid + + if cid is not None: + answer.cid = cid + else: + answer.user = request.user answer.question = question answer.exam = exam # answer.published_date = timezone.now() @@ -248,28 +276,28 @@ def exam_take(request, pk, sk, cid, passcode): cid_user_exam.end_time = timezone.now() cid_user_exam.save() - kwargs = {"pk": pk, "cid": cid, "passcode": passcode} + if cid is not None: + kwargs = {"pk": pk, "cid": cid, "passcode": passcode} + take_url = "sbas:exam_take" + finish_url = "sbas:exam_take_overview" + else: + kwargs = {"pk": pk} + take_url = "sbas:exam_take_user" + finish_url = "sbas:exam_take_overview_user" if "next" in request.POST: - return redirect("sbas:exam_take", sk=pos + 1, **kwargs) + if not next: + return HttpResponseBadRequest("Invalid request") + return redirect(take_url, sk=pos + 1, **kwargs) elif "previous" in request.POST: - return redirect("sbas:exam_take", sk=pos - 1, **kwargs) + return redirect(take_url, sk=pos - 1, **kwargs) elif "finish" in request.POST: - return redirect("sbas:exam_take_overview", **kwargs) + return redirect(finish_url, **kwargs) elif "goto" in request.POST: - return redirect( - "sbas:exam_take", sk=int(request.POST.get("goto")), **kwargs - ) + return redirect(take_url, sk=int(request.POST.get("goto")), **kwargs) else: form = UserAnswerForm(instance=answer) - previous = -1 - if sk > 0: - previous = sk - 1 - next = sk + 1 - if sk == exam_length - 1: - next = False - saved_answer = False if answer is not None: saved_answer = answer.answer @@ -293,9 +321,7 @@ def exam_take(request, pk, sk, cid, passcode): ) -GenericExamViews = ExamViews( - Exam, Question, None, UserAnswer, "sbas", "sbas" -) +GenericExamViews = ExamViews(Exam, Question, None, UserAnswer, "sbas", "sbas") class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): @@ -310,6 +336,7 @@ class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): context["app_name"] = "sbas" return context + class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): model = Question form_class = QuestionForm @@ -321,22 +348,20 @@ class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): def get_context_data(self, **kwargs): context = super(QuestionCreateBase, self).get_context_data(**kwargs) - #if self.request.POST: + # if self.request.POST: # context["answer_formset"] = AnswerFormSet( # self.request.POST, self.request.FILES # ) - #else: + # else: # context["answer_formset"] = AnswerFormSet() return context def form_valid(self, form): - self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) - response = super().form_valid(form) # If the normal submit button is pressed we save as normal if "submit" in self.request.POST: @@ -347,7 +372,6 @@ class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): class QuestionCreate(QuestionCreateBase): - # initial = {'laterality': AnatomyQuestion.NONE} def get_initial(self): @@ -359,6 +383,7 @@ class QuestionCreate(QuestionCreateBase): return initial + class QuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): model = Question form_class = QuestionForm @@ -373,10 +398,9 @@ class QuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): return context def form_valid(self, form): - # save exam orders (there must be a better way to do this) - #exam_orders = {} - #for exam in self.object.exams.all(): + # exam_orders = {} + # for exam in self.object.exams.all(): # exam_orders[exam] = list(exam.exam_questions.all()) # print(exam_orders[exam]) @@ -389,6 +413,7 @@ class QuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): response = super().form_valid(form) return response + class QuestionClone(QuestionCreateBase): """Clones a existing question""" @@ -404,6 +429,7 @@ class QuestionClone(QuestionCreateBase): return initial_data + class UserAnswerView(LoginRequiredMixin, DetailView): model = UserAnswer @@ -450,9 +476,11 @@ class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): model = Exam form_class = ExamForm + class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): model = Exam + # class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): # # queryset = Exam.objects.all().order_by('name') # serializer_class = ExamSerializer @@ -468,11 +496,13 @@ class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): # # return Exam.objects.filter(author__id=user.id) -class ExamAuthorUpdate(RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView): + +class ExamAuthorUpdate( + RevisionMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView +): model = Exam form_class = ExamAuthorForm - class ExamClone(ExamCloneMixin, ExamCreate): - """Clone exam view""" \ No newline at end of file + """Clone exam view""" diff --git a/templates/base.html b/templates/base.html index a05a9a14..e0413561 100644 --- a/templates/base.html +++ b/templates/base.html @@ -128,7 +128,6 @@