allow supervisors to view shared exams

This commit is contained in:
Ross
2024-11-04 15:18:04 +00:00
parent eab6fc9d7e
commit 80358e2113
4 changed files with 120 additions and 36 deletions
+16 -5
View File
@@ -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/<int:cid>/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/<int:pk>/overview",
views.supervisor_overview,
@@ -225,6 +227,11 @@ urlpatterns = [
views.ExamCollectionDelete.as_view(),
name="examcollection_delete",
),
path(
"share_with_supervisor_toggle/<int:pk>/",
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/<int:exam_id>/order_questions", generic_exam_view.order_questions, name="order_questions"),
path(
"exam/<int:exam_id>/order_questions",
generic_exam_view.order_questions,
name="order_questions",
),
path("exam/<int:exam_id>/cids", generic_exam_view.exam_cids, name="exam_cids"),
# path("exam/<int:exam_id>/groups", generic_exam_view.exam_groups_edit, name="exam_groups_edit"),
path(
+76 -23
View File
@@ -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),
}
kwargs = {
"exam_mode": True,
"archive": False
"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}
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
@@ -2124,8 +2140,8 @@ class ExamViews(View, LoginRequiredMixin):
raise Exception("user must be defined")
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,
@@ -3831,7 +3848,6 @@ def supervisor_overview(request, pk):
except Supervisor.RelatedObjectDoesNotExist:
raise PermissionDenied()
trainees = User.objects.filter(userprofile__supervisor=supervisor)
return render(
@@ -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")
@@ -3880,7 +3897,12 @@ def supervisor_request_account(request):
return HttpResponse(f"Invalid email address ({email})")
if supervisor.user:
return HttpResponse(format_html("User already exists. Reset your password <a href='{}'>here</a>", reverse("password_reset")))
return HttpResponse(
format_html(
"User already exists. Reset your password <a href='{}'>here</a>",
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 <a href='{}'>here</a>", reverse("password_reset")))
return HttpResponse(
format_html(
"Account created, you now need to reset your password <a href='{}'>here</a>",
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()
+13
View File
@@ -2,6 +2,19 @@
{% block content %}
<h1>Examinations</h1>
<details class="help-text">
<summary><i class="bi bi-info-circle"></i> Help</summary>
<p>This page shows the currently active examations (that you have access to manage).</p>
<p>Exams that are active will be available for candidates to take.</p>
<p>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.</p>
<p>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.</p>
<p>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).</p>
</details>
<div class="{{app_name}}">
Active exams:<br/>
<ul class="exam-list exam-list-active">
+10 -3
View File
@@ -31,9 +31,16 @@
{% endif %}
<h2>Exam: {{ exam }}</h2>
<h3>Candidate: {{ cid|default_if_none:request.user.username }}</h3>
{% if exam.results_supervisor_visible %}
{% if cid_user_exam %}
{% if exam.results_supervisor_visible %}
Exam results are visible to supervisor.<br/>
{% else %}
Share results with supervisor: {{cid_user_exam.share_with_supervisor}}<br/>
{% else %}
Share results with supervisor: {{cid_user_exam.share_with_supervisor}} <button
class="btn btn-sm"
hx-get="{% url 'generic:toggle_share_with_supervisor' cid_user_exam.pk %}"
>Toggle</button>
<br/>
{% endif %}
{% endif %}
Answers: