numerous supervisor upgrades
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('anatomy', '0023_exam_exam_open_access'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0062_alter_casecollection_markers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='casecollection',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -201,6 +201,7 @@ class ExamFormMixin:
|
|||||||
"active",
|
"active",
|
||||||
"publish_results",
|
"publish_results",
|
||||||
"archive",
|
"archive",
|
||||||
|
"results_supervisor_visible"
|
||||||
# "cid_user_groups",
|
# "cid_user_groups",
|
||||||
# "user_user_groups",
|
# "user_user_groups",
|
||||||
# "author",
|
# "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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -602,6 +602,11 @@ class ExamOrCollectionGenericBase(models.Model, AuthorMixin):
|
|||||||
default=False,
|
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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
@@ -1463,6 +1468,8 @@ class CidUserExam(models.Model):
|
|||||||
# TODO switch to json field?
|
# TODO switch to json field?
|
||||||
results_emailed_status = models.CharField(max_length=255, blank=True)
|
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:
|
def __str__(self) -> str:
|
||||||
if self.cid_user is None:
|
if self.cid_user is None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<a href="{% url 'generic:manage_cid_exams' %}">Cids (exams)</a> /
|
<a href="{% url 'generic:manage_cid_exams' %}">Cids (exams)</a> /
|
||||||
<a href="{% url 'generic:cid_group_view' %}">Cid Groups</a> /
|
<a href="{% url 'generic:cid_group_view' %}">Cid Groups</a> /
|
||||||
<a href="{% url 'generic:user_group_view' %}">User 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 'accounts_list' %}">Manage Users</a> /
|
||||||
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</a> /
|
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</a> /
|
||||||
<a href="{% url 'generic:examcollection_list' %}">Collections</a>
|
<a href="{% url 'generic:examcollection_list' %}">Collections</a>
|
||||||
|
|||||||
+23
@@ -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(s): {% for author in exam.author.all %}
|
||||||
{{ author }}{% if not forloop.last %}, {% endif %}
|
{{ author }}{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}<br/>
|
{% 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>
|
<details>
|
||||||
<summary>Extra</summary>
|
<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 %}
|
||||||
|
|
||||||
@@ -166,6 +166,17 @@ urlpatterns = [
|
|||||||
path("cids/<int:cid>/details", views.cid_details, name="cid_details"),
|
path("cids/<int:cid>/details", views.cid_details, name="cid_details"),
|
||||||
path("cids/create/email", views.create_cid_email, name="create_cid_email"),
|
path("cids/create/email", views.create_cid_email, name="create_cid_email"),
|
||||||
path("supervisor", views.SupervisorList.as_view(), name="supervisor"),
|
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(
|
path(
|
||||||
"supervisor/<int:pk>",
|
"supervisor/<int:pk>",
|
||||||
views.SupervisorDetail.as_view(),
|
views.SupervisorDetail.as_view(),
|
||||||
@@ -342,6 +353,11 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
|
|||||||
generic_exam_view.exam_scores_user_admin,
|
generic_exam_view.exam_scores_user_admin,
|
||||||
name="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(
|
path(
|
||||||
"exam/<int:pk>/toggle_active",
|
"exam/<int:pk>/toggle_active",
|
||||||
generic_exam_view.exam_toggle_active,
|
generic_exam_view.exam_toggle_active,
|
||||||
|
|||||||
+136
-6
@@ -98,12 +98,14 @@ from rapids.models import (
|
|||||||
ExamQuestionDetail as RapidsExamQuestionDetail,
|
ExamQuestionDetail as RapidsExamQuestionDetail,
|
||||||
)
|
)
|
||||||
from rapids.models import Exam as RapidsExam
|
from rapids.models import Exam as RapidsExam
|
||||||
|
from rapids.models import UserAnswer as RapidsUserAnswer
|
||||||
from longs.models import (
|
from longs.models import (
|
||||||
Long as LongQuestion,
|
Long as LongQuestion,
|
||||||
LongSeries,
|
LongSeries,
|
||||||
ExamQuestionDetail as LongsExamQuestionDetail,
|
ExamQuestionDetail as LongsExamQuestionDetail,
|
||||||
)
|
)
|
||||||
from longs.models import Exam as LongsExam
|
from longs.models import Exam as LongsExam
|
||||||
|
from longs.models import UserAnswer as LongsUserAnswer
|
||||||
from anatomy.models import (
|
from anatomy.models import (
|
||||||
AnatomyQuestion as AnatomyQuestion,
|
AnatomyQuestion as AnatomyQuestion,
|
||||||
ExamQuestionDetail as AnatomyExamQuestionDetail,
|
ExamQuestionDetail as AnatomyExamQuestionDetail,
|
||||||
@@ -113,12 +115,15 @@ from sbas.models import (
|
|||||||
Question as SbasQuestion,
|
Question as SbasQuestion,
|
||||||
ExamQuestionDetail as SbasExamQuestionDetail,
|
ExamQuestionDetail as SbasExamQuestionDetail,
|
||||||
)
|
)
|
||||||
|
from anatomy.models import UserAnswer as AnatomyUserAnswer
|
||||||
from sbas.models import Exam as SbasExam
|
from sbas.models import Exam as SbasExam
|
||||||
|
from sbas.models import UserAnswer as SbasUserAnswer
|
||||||
from physics.models import (
|
from physics.models import (
|
||||||
Question as PhysicsQuestion,
|
Question as PhysicsQuestion,
|
||||||
ExamQuestionDetail as PhysicsExamQuestionDetail,
|
ExamQuestionDetail as PhysicsExamQuestionDetail,
|
||||||
)
|
)
|
||||||
from physics.models import Exam as PhysicsExam
|
from physics.models import Exam as PhysicsExam
|
||||||
|
from physics.models import UserAnswer as PhysicsUserAnswer
|
||||||
|
|
||||||
from django.db.models import Case, When
|
from django.db.models import Case, When
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -164,6 +169,34 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
|
|||||||
# raise PermissionDenied() # or Http404
|
# 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:
|
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
|
||||||
EXAM_MAP = {
|
EXAM_MAP = {
|
||||||
"physics": PhysicsExam,
|
"physics": PhysicsExam,
|
||||||
@@ -494,6 +527,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if (exam.open_access and exam.active) or user in exam.get_author_objects():
|
if (exam.open_access and exam.active) or user in exam.get_author_objects():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if exam.cid_user_exam.filter(user_user=user):
|
||||||
|
return True
|
||||||
|
|
||||||
if marker and user in exam.markers.all():
|
if marker and user in exam.markers.all():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -1521,7 +1557,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
exams = self.Exam.objects.all()
|
exams = self.Exam.objects.all()
|
||||||
filter = self.ExtraExamFilter(request.GET, queryset=exams)
|
filter = self.ExtraExamFilter(request.GET, queryset=exams)
|
||||||
else:
|
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)
|
filter = self.BasicExamFilter(request.GET, queryset=exams)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@@ -2044,21 +2082,38 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
return self.exam_scores_cid_user(request, pk, user=user)
|
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))
|
@method_decorator(user_passes_test(lambda u: u.is_superuser))
|
||||||
def exam_scores_cid_user_admin(self, request, pk, cid):
|
def exam_scores_cid_user_admin(self, request, pk, cid):
|
||||||
user = CidUser.objects.get(cid=cid)
|
user = CidUser.objects.get(cid=cid)
|
||||||
return self.exam_scores_cid_user(request, pk, cid, user.passcode)
|
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)
|
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
if not exam.exam_mode:
|
if not exam.exam_mode:
|
||||||
raise Http404("Packet not in exam mode")
|
raise Http404("Packet not in exam mode")
|
||||||
|
|
||||||
if not exam.check_cid_user(cid, passcode, request.user):
|
if supervisor is None:
|
||||||
if not self.check_user_access(request.user, pk):
|
if not exam.check_cid_user(cid, passcode, request.user):
|
||||||
# raise PermissionDenied
|
#TODO clear this up
|
||||||
raise Http404("Error accessing exam")
|
# 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:
|
if user is not None:
|
||||||
request.user = user
|
request.user = user
|
||||||
@@ -2176,6 +2231,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
case _:
|
case _:
|
||||||
max_score = len(questions)
|
max_score = len(questions)
|
||||||
|
|
||||||
|
cid_user_exam = exam.cid_user_exam.filter(user_user=user).first()
|
||||||
|
|
||||||
template_context = {
|
template_context = {
|
||||||
"exam": exam,
|
"exam": exam,
|
||||||
"cid": cid,
|
"cid": cid,
|
||||||
@@ -2187,6 +2244,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
"max_score": max_score,
|
"max_score": max_score,
|
||||||
"answers_and_marks": answers_and_marks,
|
"answers_and_marks": answers_and_marks,
|
||||||
"view_all_results": view_all_results,
|
"view_all_results": view_all_results,
|
||||||
|
"supervisor": supervisor,
|
||||||
|
"cid_user_exam": cid_user_exam,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.normalise_score is not None:
|
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():
|
if self.request.user in obj.get_author_objects():
|
||||||
return context
|
return context
|
||||||
raise PermissionDenied() # or Http404
|
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", {})
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('longs', '0030_exam_exam_open_access'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('physics', '0016_exam_exam_open_access'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+7
-21
@@ -7,7 +7,7 @@ from atlas.models import CaseCollection, CidReportAnswer
|
|||||||
from generic.decorators import user_is_cid_user_manager
|
from generic.decorators import user_is_cid_user_manager
|
||||||
from generic.filters import UserUserFilter
|
from generic.filters import UserUserFilter
|
||||||
from generic.tables import UserUserTable
|
from generic.tables import UserUserTable
|
||||||
from generic.views import CidManagerRequiredMixin, RedirectMixin, get_question_and_content_type
|
from generic.views import CidManagerRequiredMixin, RedirectMixin, get_question_and_content_type, get_user_exams
|
||||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
@@ -39,9 +39,8 @@ from django.http import Http404, JsonResponse
|
|||||||
|
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
|
||||||
from physics.models import UserAnswer as PhysicsUserAnswer, Question
|
from physics.models import UserAnswer as PhysicsUserAnswer
|
||||||
from physics.models import Exam as PhysicsExam
|
from physics.models import Exam as PhysicsExam
|
||||||
from physics.models import Question as SbasQuestion
|
|
||||||
|
|
||||||
from anatomy.models import UserAnswer as AnatomyUserAnswer
|
from anatomy.models import UserAnswer as AnatomyUserAnswer
|
||||||
from anatomy.models import Exam as AnatomyExam
|
from anatomy.models import Exam as AnatomyExam
|
||||||
@@ -60,7 +59,6 @@ from longs.models import Long
|
|||||||
|
|
||||||
from sbas.models import UserAnswer as SbasUserAnswer
|
from sbas.models import UserAnswer as SbasUserAnswer
|
||||||
from sbas.models import Exam as SbasExam
|
from sbas.models import Exam as SbasExam
|
||||||
from sbas.models import Question as SbasQuestion
|
|
||||||
|
|
||||||
from anatomy.views import GenericExamViews as AnatomyExamViews
|
from anatomy.views import GenericExamViews as AnatomyExamViews
|
||||||
from rapids.views import GenericExamViews as RapidsExamsViews
|
from rapids.views import GenericExamViews as RapidsExamsViews
|
||||||
@@ -148,6 +146,7 @@ def user_scores_admin(request, user_id):
|
|||||||
|
|
||||||
return user_scores(request, user=user)
|
return user_scores(request, user=user)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_scores(request, user=None):
|
def user_scores(request, user=None):
|
||||||
# exam = get_object_or_404(Exam, pk=pk)
|
# exam = get_object_or_404(Exam, pk=pk)
|
||||||
@@ -164,23 +163,7 @@ def user_scores(request, user=None):
|
|||||||
user = request.user
|
user = request.user
|
||||||
admin = False
|
admin = False
|
||||||
|
|
||||||
EXAM_ANSWER_MAP = {
|
exams = get_user_exams(user)
|
||||||
"physics": (PhysicsUserAnswer, PhysicsExam),
|
|
||||||
"anatomy": (AnatomyUserAnswer, AnatomyExam),
|
|
||||||
"rapids": (RapidsUserAnswer, RapidsExam),
|
|
||||||
"longs": (LongsUserAnswer, LongsExam),
|
|
||||||
"sbas": (SbasUserAnswer, SbasExam),
|
|
||||||
}
|
|
||||||
kwargs = {"exam_mode": True, "archive": False}
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
# case_collections = request.user.casecollection_exams.all()
|
# case_collections = request.user.casecollection_exams.all()
|
||||||
|
|
||||||
@@ -737,6 +720,9 @@ def accounts_bulk_create_check(request):
|
|||||||
|
|
||||||
user = json.loads(request.POST.get("user"))
|
user = json.loads(request.POST.get("user"))
|
||||||
|
|
||||||
|
if "email" not in user:
|
||||||
|
return HttpResponse(format_html("<span class='error'>{}</span>", "No email provided"))
|
||||||
|
|
||||||
if User.objects.filter(username=user["email"]).exists():
|
if User.objects.filter(username=user["email"]).exists():
|
||||||
return HttpResponse(format_html("<span class='error'>{}</span>", "User already exists"))
|
return HttpResponse(format_html("<span class='error'>{}</span>", "User already exists"))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rapids', '0015_alter_rapid_normal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-10-28 11:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sbas', '0016_exam_exam_open_access'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='results_supervisor_visible',
|
||||||
|
field=models.BooleanField(default=False, help_text='If true the results will be visible to supervisors without candidate approval.'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if request.user.supervisor %}
|
||||||
|
<a href='{% url "generic:supervisor_overview" request.user.supervisor.pk %}'>Supervisor overview</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'user_scores'%}"><button>Exams</button></a>
|
<a href="{% url 'user_scores'%}"><button>Exams</button></a>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
{% if view_all_results %}
|
{% if view_all_results %}
|
||||||
|
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
Exam state:
|
Exam state:
|
||||||
|
|
||||||
@@ -24,6 +25,15 @@
|
|||||||
<div class="alert alert-info" role="alert">Results are not currently published. You can see your own answers below.</a></div>
|
<div class="alert alert-info" role="alert">Results are not currently published. You can see your own answers below.</a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if supervisor %}
|
||||||
|
<div class="alert alert-info" role="alert">Viewing results as a <a href='{% url "generic:supervisor_overview" supervisor.supervisor.pk %}'>supervisor</a>.</div>
|
||||||
|
{% endif %}
|
||||||
<h2>Exam: {{ exam }}</h2>
|
<h2>Exam: {{ exam }}</h2>
|
||||||
<h3>Candidate: {{ cid|default_if_none:request.user.username }}</h3>
|
<h3>Candidate: {{ cid|default_if_none:request.user.username }}</h3>
|
||||||
|
{% if exam.results_supervisor_visible %}
|
||||||
|
Exam results are visible to supervisor.<br/>
|
||||||
|
{% else %}
|
||||||
|
Share results with supervisor: {{cid_user_exam.share_with_supervisor}}<br/>
|
||||||
|
{% endif %}
|
||||||
Answers:
|
Answers:
|
||||||
Reference in New Issue
Block a user