numerous supervisor upgrades

This commit is contained in:
Ross
2024-10-28 16:30:13 +00:00
parent 375261dd84
commit a2907fa285
21 changed files with 455 additions and 27 deletions
+1
View File
@@ -201,6 +201,7 @@ class ExamFormMixin:
"active",
"publish_results",
"archive",
"results_supervisor_visible"
# "cid_user_groups",
# "user_user_groups",
# "author",
@@ -0,0 +1,18 @@
# Generated by Django 5.0.2 on 2024-10-28 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0018_alter_supervisor_user'),
]
operations = [
migrations.AddField(
model_name='examuserstatus',
name='share_with_supervisor',
field=models.BooleanField(default=False, help_text='If true the exam status and results will be available to a users supervisor'),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.0.2 on 2024-10-28 13:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('generic', '0019_examuserstatus_share_with_supervisor'),
]
operations = [
migrations.RemoveField(
model_name='examuserstatus',
name='share_with_supervisor',
),
migrations.AddField(
model_name='ciduserexam',
name='share_with_supervisor',
field=models.BooleanField(default=False, help_text='If true the exam status and results will be available to a users supervisor'),
),
]
+7
View File
@@ -602,6 +602,11 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
default=False,
)
results_supervisor_visible = models.BooleanField(
help_text="If true the results will be visible to supervisors without candidate approval.",
default=False,
)
class Meta:
abstract = True
@@ -1463,6 +1468,8 @@ class CidUserExam(models.Model):
# TODO switch to json field?
results_emailed_status = models.CharField(max_length=255, blank=True)
share_with_supervisor = models.BooleanField(default=False, help_text="If true the exam status and results will be available to a users supervisor")
def __str__(self) -> str:
if self.cid_user is None:
try:
+1
View File
@@ -21,6 +21,7 @@
<a href="{% url 'generic:manage_cid_exams' %}">Cids (exams)</a> /
<a href="{% url 'generic:cid_group_view' %}">Cid Groups</a> /
<a href="{% url 'generic:user_group_view' %}">User Groups</a> /
<a href="{% url 'trainees' %}">Trainees</a> /
<a href="{% url 'accounts_list' %}">Manage Users</a> /
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</a> /
<a href="{% url 'generic:examcollection_list' %}">Collections</a>
+23
View File
@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block title %}
Supervisor
{% endblock %}
{% block css %}
{% endblock %}
{% block js %}
{% endblock %}
{% block content %}
{% endblock %}
{% block navigation %}
Supervisor:
{% if request.user.is_authenticated %}
<a href='{% url "generic:supervisor_overview" supervisor.pk %}'>Overview</a> /
{% endif %}
{% endblock %}
@@ -113,6 +113,7 @@ Open access: {{ exam.open_access }}<br />
Author(s): {% for author in exam.author.all %}
{{ author }}{% if not forloop.last %}, {% endif %}
{% endfor %}<br/>
<span title="If true the results from this exam will be visible to supervisors automatically. If false the trainee will have to manually approve access.">Supervisor visible: {{exam.results_supervisor_visible}}</span><br/>
<details>
<summary>Extra</summary>
@@ -0,0 +1,43 @@
{% extends 'generic/base_supervisor.html' %}
{% load crispy_forms_tags %}
{% block content %}
<h2>Supervisor: {{supervisor.name}}</h2>
<details class="help-text">
<summary><i class="bi bi-info-circle"></i> Help</summary>
<p>As a supervisor you have the ability to view results on the platform for your associated trainees.</p>
<p>Some exams / collection results will be automatically available for you to review, others may require the trainee to allow access</p>
<p>If your trainees are incorrect please contact ross.kruger@nhs.net</p>
</details>
<h4>Trainees</h4>
You have the following trainee(s):
<ul id="trainee-list">
{% for trainee in trainees %}
<li>
<i class="bi bi-file-person"></i>
<a href='{% url "generic:supervisor_trainee" supervisor.pk trainee.pk %}'>{{trainee.first_name}} {{trainee.last_name}}, {{trainee.email}}</a>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block css %}
<style>
#trainee-list {
list-style-type: none;
padding: 0;
}
</style>
{% endblock css %}
@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block css %}
{% endblock %}
{% block content %}
This form allows you to create an account if you are a supervisor. To do so please enter your email address.
<form>
<input type="text" name="email" id="email" value="" required>
<button
hx-post="{% url 'generic:supervisor_request_account' %}"
hx-target="#result"
>Request Account</button>
</form>
<div id="result"></div>
{% endblock %}
@@ -0,0 +1,38 @@
{% extends 'generic/base_supervisor.html' %}
{% load crispy_forms_tags %}
{% block css %}
{% endblock %}
{% block content %}
<h2>Trainee: {{trainee.first_name}} {{trainee.last_name}}</h2>
email: {{trainee.email}}
<h3>Results</h3>
<div id="exam-results">
{% if all_exams %}
The following exam results for the trainee are available.
{% for exam_type, exams in all_exams %}
{% if exams %}
<h4>{{exam_type|title}}</h4>
<ul class='{{exam_type|lower}}'>
{% for exam in exams %}
<li class="exam" data-exam-id="{{exam.pk}}">
<a href= "{% url exam_type|add:':exam_scores_user_supervisor' pk=exam.pk user_id=trainee.pk %}" title="Click to view results">
{{exam}}</a> {% if exam.active %}[Active]{% endif %} {% if exam.publish_results %}[Results Published]{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
{% endfor %}
{% else %}
No results are available for this trainee.
{% endif %}
</div>
{% endblock %}
+16
View File
@@ -166,6 +166,17 @@ 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/<int:pk>/overview",
views.supervisor_overview,
name="supervisor_overview",
),
path(
"supervisor/<int:pk>/trainee/<int:trainee_id>/",
views.supervisor_trainee,
name="supervisor_trainee",
),
path(
"supervisor/<int:pk>",
views.SupervisorDetail.as_view(),
@@ -342,6 +353,11 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
generic_exam_view.exam_scores_user_admin,
name="exam_scores_user_admin",
),
path(
"exam/<int:pk>/scores/<int:user_id>/user/supervisor",
generic_exam_view.exam_scores_user_supervisor,
name="exam_scores_user_supervisor",
),
path(
"exam/<int:pk>/toggle_active",
generic_exam_view.exam_toggle_active,
+136 -6
View File
@@ -98,12 +98,14 @@ from rapids.models import (
ExamQuestionDetail as RapidsExamQuestionDetail,
)
from rapids.models import Exam as RapidsExam
from rapids.models import UserAnswer as RapidsUserAnswer
from longs.models import (
Long as LongQuestion,
LongSeries,
ExamQuestionDetail as LongsExamQuestionDetail,
)
from longs.models import Exam as LongsExam
from longs.models import UserAnswer as LongsUserAnswer
from anatomy.models import (
AnatomyQuestion as AnatomyQuestion,
ExamQuestionDetail as AnatomyExamQuestionDetail,
@@ -113,12 +115,15 @@ from sbas.models import (
Question as SbasQuestion,
ExamQuestionDetail as SbasExamQuestionDetail,
)
from anatomy.models import UserAnswer as AnatomyUserAnswer
from sbas.models import Exam as SbasExam
from sbas.models import UserAnswer as SbasUserAnswer
from physics.models import (
Question as PhysicsQuestion,
ExamQuestionDetail as PhysicsExamQuestionDetail,
)
from physics.models import Exam as PhysicsExam
from physics.models import UserAnswer as PhysicsUserAnswer
from django.db.models import Case, When
from django.conf import settings
@@ -164,6 +169,34 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
# raise PermissionDenied() # or Http404
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
}
if supervisor_view:
kwargs["results_supervisor_visible"] = True
exams = []
for exam_type in EXAM_ANSWER_MAP:
UserAnswer, Exam = 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 exams_to_add:
exams.append((exam_type, exams_to_add))
return exams
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
EXAM_MAP = {
"physics": PhysicsExam,
@@ -494,6 +527,9 @@ class ExamViews(View, LoginRequiredMixin):
if (exam.open_access and exam.active) or user in exam.get_author_objects():
return True
if exam.cid_user_exam.filter(user_user=user):
return True
if marker and user in exam.markers.all():
return True
@@ -1521,7 +1557,9 @@ class ExamViews(View, LoginRequiredMixin):
exams = self.Exam.objects.all()
filter = self.ExtraExamFilter(request.GET, queryset=exams)
else:
exams = self.Exam.objects.filter(author__id=request.user.id) | self.Exam.objects.filter(open_access=True)
exams = self.Exam.objects.filter(
author__id=request.user.id
) | self.Exam.objects.filter(open_access=True)
filter = self.BasicExamFilter(request.GET, queryset=exams)
return render(
@@ -2044,21 +2082,38 @@ class ExamViews(View, LoginRequiredMixin):
user = User.objects.get(id=user_id)
return self.exam_scores_cid_user(request, pk, user=user)
def exam_scores_user_supervisor(self, request, pk, user_id):
user = User.objects.get(id=user_id)
if not request.user.supervisor == user.userprofile.supervisor:
raise PermissionDenied
return self.exam_scores_cid_user(request, pk, user=user, supervisor=request.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):
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:
raise Http404("Packet not in exam mode")
if not exam.check_cid_user(cid, passcode, request.user):
if not self.check_user_access(request.user, pk):
# raise PermissionDenied
raise Http404("Error accessing exam")
if supervisor is None:
if not exam.check_cid_user(cid, passcode, request.user):
#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:
# pass
# check for supervisor access
raise Http404("Error accessing exam")
else:
if user is None:
raise Exception("user must be defined")
if user is not None:
request.user = user
@@ -2176,6 +2231,8 @@ class ExamViews(View, LoginRequiredMixin):
case _:
max_score = len(questions)
cid_user_exam = exam.cid_user_exam.filter(user_user=user).first()
template_context = {
"exam": exam,
"cid": cid,
@@ -2187,6 +2244,8 @@ class ExamViews(View, LoginRequiredMixin):
"max_score": max_score,
"answers_and_marks": answers_and_marks,
"view_all_results": view_all_results,
"supervisor": supervisor,
"cid_user_exam": cid_user_exam,
}
if self.normalise_score is not None:
@@ -3754,3 +3813,74 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
if self.request.user in obj.get_author_objects():
return context
raise PermissionDenied() # or Http404
def supervisor_overview(request, pk):
supervisor = get_object_or_404(Supervisor, pk=pk)
if not request.user.supervisor == supervisor:
raise PermissionDenied()
trainees = User.objects.filter(userprofile__supervisor=supervisor)
return render(
request,
"generic/supervisor_overview.html",
{"supervisor": supervisor, "trainees": trainees},
)
def supervisor_trainee(request, pk, trainee_id):
supervisor = get_object_or_404(Supervisor, pk=pk)
if not request.user.supervisor == supervisor:
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)
exams = get_user_exams(trainee, supervisor_view=True)
print(trainee)
return render(
request,
"generic/supervisor_trainee.html",
{"supervisor": supervisor, "trainee": trainee, "all_exams": exams},
)
def supervisor_request_account(request):
if request.method == "POST":
email = request.POST.get("email")
try:
validate_email(email)
except ValidationError:
return HttpResponse(f"Invalid email address ({email})")
try:
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 <a href='{}'>here</a>", reverse("password_reset")))
try:
user_account = User.objects.get(email=email)
except User.DoesNotExist:
try:
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)
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")))
else:
return render(request, "generic/supervisor_request_account.html", {})