diff --git a/generic/urls.py b/generic/urls.py index 8e17b889..23a57095 100755 --- a/generic/urls.py +++ b/generic/urls.py @@ -44,9 +44,7 @@ urlpatterns = [ ), path( "supervisor-autocomplete", - views.SupervisorAutocomplete.as_view( - model=Supervisor - ), + views.SupervisorAutocomplete.as_view(model=Supervisor), name="supervisor-autocomplete", ), path( @@ -166,7 +164,11 @@ urlpatterns = [ path("cids//details", views.cid_details, name="cid_details"), path("cids/create/email", views.create_cid_email, name="create_cid_email"), path("supervisor", views.SupervisorList.as_view(), name="supervisor"), - path("supervisor/request_account", views.supervisor_request_account, name="supervisor_request_account"), + path( + "supervisor/request_account", + views.supervisor_request_account, + name="supervisor_request_account", + ), path( "supervisor//overview", views.supervisor_overview, @@ -225,6 +227,11 @@ urlpatterns = [ views.ExamCollectionDelete.as_view(), name="examcollection_delete", ), + path( + "share_with_supervisor_toggle//", + views.toggle_share_with_supervisor, + name="toggle_share_with_supervisor", + ), ] @@ -380,7 +387,11 @@ def generic_exam_urls(generic_exam_view: GenericExamViews): ), path("exam/", generic_exam_view.exam_list, name="exam_list"), path("exam/all", generic_exam_view.exam_list_all, name="exam_list_all"), - path("exam//order_questions", generic_exam_view.order_questions, name="order_questions"), + path( + "exam//order_questions", + generic_exam_view.order_questions, + name="order_questions", + ), path("exam//cids", generic_exam_view.exam_cids, name="exam_cids"), # path("exam//groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"), path( diff --git a/generic/views.py b/generic/views.py index 65d4b9f6..e5d3467d 100644 --- a/generic/views.py +++ b/generic/views.py @@ -79,6 +79,7 @@ from .forms import ( from .models import ( CidUser, + CidUserExam, CidUserGroup, ExamBase, ExamCollection, @@ -171,27 +172,34 @@ class CidManagerRequiredMixin(UserPassesTestMixin): def get_user_exams(user, supervisor_view=False): EXAM_ANSWER_MAP = { - "physics": (PhysicsUserAnswer, PhysicsExam), - "anatomy": (AnatomyUserAnswer, AnatomyExam), - "rapids": (RapidsUserAnswer, RapidsExam), - "longs": (LongsUserAnswer, LongsExam), - "sbas": (SbasUserAnswer, SbasExam), + "physics": (PhysicsUserAnswer, PhysicsExam, "physics_exams"), + "anatomy": (AnatomyUserAnswer, AnatomyExam, "anatomy_exams"), + "rapids": (RapidsUserAnswer, RapidsExam, "rapids_exams"), + "longs": (LongsUserAnswer, LongsExam, "longs_exams"), + "sbas": (SbasUserAnswer, SbasExam, "sbas_exams"), } - kwargs = { - "exam_mode": True, - "archive": False - } + kwargs = {"exam_mode": True, "archive": False} + + exams = [] if supervisor_view: + kwargs["results_supervisor_visible"] = True del kwargs["archive"] - exams = [] for exam_type in EXAM_ANSWER_MAP: - UserAnswer, Exam = EXAM_ANSWER_MAP[exam_type] + UserAnswer, Exam, cid_rel = EXAM_ANSWER_MAP[exam_type] exam_answers = UserAnswer.objects.filter(user=user) exam_ids = exam_answers.values_list("exam").distinct() exams_to_add = Exam.objects.filter(id__in=exam_ids, **kwargs) + if supervisor_view: + extra_exam_ids = CidUserExam.objects.filter( + user_user=user, + share_with_supervisor=True, + content_type=ContentType.objects.get_for_model(Exam), + ).values_list(cid_rel) + + exams_to_add = exams_to_add | Exam.objects.filter(id__in=extra_exam_ids) if exams_to_add: exams.append((exam_type, exams_to_add)) @@ -528,7 +536,11 @@ class ExamViews(View, LoginRequiredMixin): if (exam.open_access and exam.active) or user in exam.get_author_objects(): return True - if user.is_authenticated and exam.active and exam.cid_user_exam.filter(user_user=user): + if ( + user.is_authenticated + and exam.active + and exam.cid_user_exam.filter(user_user=user) + ): return True if marker and user in exam.markers.all(): @@ -2091,14 +2103,18 @@ class ExamViews(View, LoginRequiredMixin): if not request.user.supervisor == user.userprofile.supervisor: raise PermissionDenied - return self.exam_scores_cid_user(request, pk, user=user, supervisor=user.userprofile.supervisor.user) + return self.exam_scores_cid_user( + request, pk, user=user, supervisor=user.userprofile.supervisor.user + ) @method_decorator(user_passes_test(lambda u: u.is_superuser)) def exam_scores_cid_user_admin(self, request, pk, cid): user = CidUser.objects.get(cid=cid) return self.exam_scores_cid_user(request, pk, cid, user.passcode) - def exam_scores_cid_user(self, request, pk, cid=None, passcode="", user=None, supervisor=None): + def exam_scores_cid_user( + self, request, pk, cid=None, passcode="", user=None, supervisor=None + ): exam: ExamBase = get_object_or_404(self.Exam, pk=pk) if not exam.exam_mode: @@ -2110,11 +2126,11 @@ class ExamViews(View, LoginRequiredMixin): if supervisor is None: if not exam.check_cid_user(cid, passcode, request.user): - #TODO clear this up + # TODO clear this up # Should users be able to access results for exams they do not have access to (if they have results)? if not self.check_user_access(request.user, pk): # raise PermissionDenied - #if request.user.supervisor == user.userprofile.supervisor: + # if request.user.supervisor == user.userprofile.supervisor: # pass # check for supervisor access @@ -2122,10 +2138,10 @@ class ExamViews(View, LoginRequiredMixin): else: if user is None: raise Exception("user must be defined") - view_all_results = True + view_all_results = True - if user is not None: - request.user = user + if user is None: + user = request.user questions = ( exam.get_questions() @@ -2142,7 +2158,6 @@ class ExamViews(View, LoginRequiredMixin): answers_marks = [] answers = [] - for q in questions: # Get user answer if cid is not None: @@ -2238,6 +2253,8 @@ class ExamViews(View, LoginRequiredMixin): max_score = len(questions) cid_user_exam = exam.cid_user_exam.filter(user_user=user).first() + print(user) + print(f"{cid_user_exam=}") template_context = { "exam": exam, @@ -3829,8 +3846,7 @@ def supervisor_overview(request, pk): if not request.user.supervisor == supervisor: raise PermissionDenied() except Supervisor.RelatedObjectDoesNotExist: - raise PermissionDenied() - + raise PermissionDenied() trainees = User.objects.filter(userprofile__supervisor=supervisor) @@ -3849,7 +3865,7 @@ def supervisor_trainee(request, pk, trainee_id): if not request.user.supervisor == supervisor: raise PermissionDenied() except Supervisor.RelatedObjectDoesNotExist: - raise PermissionDenied() + raise PermissionDenied() # We do this to check that the supervisor can (should) see the trainee results trainee = User.objects.get(userprofile__supervisor=supervisor, pk=trainee_id) @@ -3866,6 +3882,7 @@ def supervisor_trainee(request, pk, trainee_id): {"supervisor": supervisor, "trainee": trainee, "all_exams": exams}, ) + def supervisor_request_account(request): if request.method == "POST": email = request.POST.get("email") @@ -3878,9 +3895,14 @@ def supervisor_request_account(request): supervisor = Supervisor.objects.get(email=email) except Supervisor.DoesNotExist: return HttpResponse(f"Invalid email address ({email})") - + if supervisor.user: - return HttpResponse(format_html("User already exists. Reset your password here", reverse("password_reset"))) + return HttpResponse( + format_html( + "User already exists. Reset your password here", + reverse("password_reset"), + ) + ) try: user_account = User.objects.get(email=email) @@ -3891,13 +3913,44 @@ def supervisor_request_account(request): first, last = supervisor.name.split(" ", 1) except ValueError: first, last = supervisor.name, "" - user_account = User.objects.create_user(email=email, username=email, password=secrets.token_hex(nbytes=16), first_name=first, last_name=last) + user_account = User.objects.create_user( + email=email, + username=email, + password=secrets.token_hex(nbytes=16), + first_name=first, + last_name=last, + ) supervisor.user = user_account supervisor.save() - return HttpResponse(format_html("Account created, you now need to reset your password here", reverse("password_reset"))) + return HttpResponse( + format_html( + "Account created, you now need to reset your password here", + reverse("password_reset"), + ) + ) else: return render(request, "generic/supervisor_request_account.html", {}) + + +def toggle_share_with_supervisor(request, pk): + if request.htmx: + cid_user_exam = get_object_or_404(CidUserExam, pk=pk) + + print(request.user) + print(cid_user_exam) + print(cid_user_exam.user_user) + if request.user != cid_user_exam.user_user: + return HttpResponse("Incorrect permissions") + + cid_user_exam.share_with_supervisor = not cid_user_exam.share_with_supervisor + cid_user_exam.save() + + return HttpResponse( + f"Shared with supervisor: {cid_user_exam.share_with_supervisor}" + ) + else: + raise PermissionDenied() diff --git a/templates/exam_list.html b/templates/exam_list.html index 3efcc860..2d90f8cd 100644 --- a/templates/exam_list.html +++ b/templates/exam_list.html @@ -2,6 +2,19 @@ {% block content %}

Examinations

+ +
+ Help +

This page shows the currently active examations (that you have access to manage).

+ +

Exams that are active will be available for candidates to take.

+ +

Once candidates have completed the exam (or subimitted some answers) they will immediately be able to view their answers on the platoform. They will not be able to see the correct answers (or their scores) until the exams results have been published.

+ +

A users submitted answers will remain available to the user until the exam is archived. Once an exam is archived the user will no longer be able to view their submitted answers.

+ +

Supervisors will have access to all exam results (including archived exams) for which they have access (when "results supervisor visible" is set to true or the user has shared their results).

+
Active exams:
    diff --git a/templates/user_score_header.html b/templates/user_score_header.html index 9e9604c5..b3788c4d 100644 --- a/templates/user_score_header.html +++ b/templates/user_score_header.html @@ -31,9 +31,16 @@ {% endif %}

    Exam: {{ exam }}

    Candidate: {{ cid|default_if_none:request.user.username }}

    -{% if exam.results_supervisor_visible %} - Exam results are visible to supervisor.
    -{% else %} - Share results with supervisor: {{cid_user_exam.share_with_supervisor}}
    + +{% if cid_user_exam %} + {% if exam.results_supervisor_visible %} + Exam results are visible to supervisor.
    + {% else %} + Share results with supervisor: {{cid_user_exam.share_with_supervisor}} +
    + {% endif %} {% endif %} Answers: \ No newline at end of file