Further candidate management improvements

This commit is contained in:
Ross
2022-11-28 15:48:50 +00:00
parent 77687327ea
commit 65b1d2da3c
11 changed files with 219 additions and 59 deletions
+2 -1
View File
@@ -22,7 +22,7 @@ from .models import (
QuestionType, QuestionType,
Exam, Exam,
) )
from generic.models import Examination from generic.models import CidUserGroup, Examination
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput, Textarea from django.forms.widgets import RadioSelect, TextInput, Textarea
@@ -204,6 +204,7 @@ class BodyPartForm(ModelForm):
class ExamForm(ExamFormMixin, ModelForm): class ExamForm(ExamFormMixin, ModelForm):
class Meta(ExamFormMixin.Meta): class Meta(ExamFormMixin.Meta):
model = Exam model = Exam
+4
View File
@@ -907,4 +907,8 @@ tr:has(.errorlist){
form .submit-button { form .submit-button {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 10px;
} }
+17
View File
@@ -60,6 +60,23 @@ from sbas.models import Question as SbasQuestion
class ExamFormMixin: class ExamFormMixin:
def __init__(self, *args, user, **kwargs) -> None:
super(ModelForm, self).__init__(*args, **kwargs)
if user.is_superuser or user.groups.filter(name="cid_manager").exists():
cid_user_group_queryset = CidUserGroup.objects.all()
user_user_group_queryset = UserUserGroup.objects.all()
else:
cid_user_group_queryset = CidUserGroup.objects.none()
user_user_group_queryset = UserUserGroup.objects.none()
self.fields["cid_user_groups"] = ModelMultipleChoiceField(
required=False,
queryset=cid_user_group_queryset,
)
self.fields["user_user_groups"] = ModelMultipleChoiceField(
required=False,
queryset=user_user_group_queryset,
)
class Meta: class Meta:
fields = [ fields = [
"name", "name",
+46 -10
View File
@@ -422,16 +422,16 @@ class QuestionNote(models.Model):
def get_short_str(self): def get_short_str(self):
return f"{self.note_type} - {self.note}" return f"{self.note_type} - {self.note}"
EXAM_TYPES = (
("Physics", "physics_exams"),
("Rapids", "rapid_exams"),
("SBAs", "sba_exams"),
("Anatomy", "anatomy_exams"),
("Longs", "longs_exams"),
("CaseCollection", "casecollection_exams"),
)
class CidUser(models.Model): class CidUser(models.Model):
EXAM_TYPES = (
("Physics", "physics_exams"),
("Rapids", "rapid_exams"),
("SBAs", "sba_exams"),
("Anatomy", "anatomy_exams"),
("Longs", "longs_exams"),
("CaseCollection", "casecollection_exams"),
)
cid = models.IntegerField(blank=True, null=True, unique=True) cid = models.IntegerField(blank=True, null=True, unique=True)
passcode = models.CharField(blank=True, max_length=50) passcode = models.CharField(blank=True, max_length=50)
@@ -463,13 +463,13 @@ class CidUser(models.Model):
def get_cid_exams(self): def get_cid_exams(self):
available_exams = [] available_exams = []
for n, t in self.EXAM_TYPES: for n, t in EXAM_TYPES:
exam_rel = getattr(self, t) exam_rel = getattr(self, t)
if exam_rel.exists(): if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name") exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams.append((n, exams)) available_exams.append((n, exams))
return available_exams return dict(available_exams)
def generate_exam_report(self): def generate_exam_report(self):
exam_list = self.get_cid_exams() exam_list = self.get_cid_exams()
@@ -655,6 +655,13 @@ class CidUserGroup(models.Model):
return reverse("generic:cid_group_update", kwargs={"pk": self.pk}) return reverse("generic:cid_group_update", kwargs={"pk": self.pk})
USER_GROUP_EXAMS = (
("SBAs", "sba_user_user_groups"),
("Physics", "physics_user_user_groups"),
("Anatomy", "anatomy_user_user_groups"),
("Rapids", "rapid_user_user_groups"),
("Longs", "longs_user_user_groups"),
)
class UserUserGroup(models.Model): class UserUserGroup(models.Model):
name = models.CharField(blank=True, max_length=50, help_text="Name of the User Group") name = models.CharField(blank=True, max_length=50, help_text="Name of the User Group")
@@ -675,6 +682,15 @@ class UserUserGroup(models.Model):
s = ", ".join([f"{user.id} - {user.username} ({user.email})" for user in users]) s = ", ".join([f"{user.id} - {user.username} ({user.email})" for user in users])
return s return s
def GetGroupExams(self):
exams = defaultdict(list)
for t, rel in USER_GROUP_EXAMS:
exam_rel = getattr(self, rel)
if exam_rel.exists():
exams[t].extend(exam_rel.all())
return dict(exams)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("generic:user_group_update", kwargs={"pk": self.pk}) return reverse("generic:user_group_update", kwargs={"pk": self.pk})
@@ -684,6 +700,15 @@ class UserGrades(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
USER_EXAM_TYPES = (
("Physics", "user_physics_exams"),
("Rapids", "user_rapid_exams"),
("SBAs", "user_sba_exams"),
("Anatomy", "user_anatomy_exams"),
("Longs", "user_longs_exams"),
#("CaseCollection", "user_casecollection_exams"),
)
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
supervisor = models.ForeignKey("Supervisor", on_delete=models.SET_NULL, blank=True, null=True, related_name="trainee") supervisor = models.ForeignKey("Supervisor", on_delete=models.SET_NULL, blank=True, null=True, related_name="trainee")
@@ -700,6 +725,17 @@ class UserProfile(models.Model):
username = property(getusername) username = property(getusername)
def get_exams(self):
available_exams = defaultdict(list)
for n, t in USER_EXAM_TYPES:
exam_rel = getattr(self.user, t)
if exam_rel.exists():
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
available_exams[n].extend(exams)
return dict(available_exams)
class Supervisor(models.Model): class Supervisor(models.Model):
"""Model to hold individual supervisor details (email and name) """Model to hold individual supervisor details (email and name)
+32 -19
View File
@@ -6,33 +6,46 @@
{% block css %} {% block css %}
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>--> <!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}} {{form.media}}
<script type="text/javascript"> <script type="text/javascript">
</script> </script>
<!-- {{ form.media }} --> <!-- {{ form.media }} -->
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Edit CID User / {{ciduser.cid}}</h2> <h2>Edit CID User / {{ciduser.cid}}</h2>
Use this form to edit a CID user. Use this form to edit a CID user.
<form action="" method="post" enctype="multipart/form-data" id="condition-form"> <form action="" method="post" enctype="multipart/form-data" id="condition-form">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<input type="submit" class="submit-button" value="Submit" name="submit"> <input type="submit" class="submit-button" value="Submit" name="submit">
</form> </form>
<h2>Exams</h2> <h2>Exams</h2>
<p>The user is currently part of the following exams</p> <p>The user is currently part of the following exams</p>
<p> <p>
{{ object.get_cid_exams }} {% with object.get_cid_exams as exam_map %}
</p> {% for exam_type, exams in exam_map.items %}
<h4>{{exam_type}}</h4>
<ul>
{% for exam in exams %}
<li>
<a href="{{exam.get_absolute_url}}">{{exam}}</a>
</li>
{% endfor %}
</ul>
{% empty %}
No exams.
{% endfor %}
{% endwith %}
</p>
<p> <p>
These should be edited via the exam candidate page. If you need to manually toggle you can do so <a href="{% url 'generic:update_cid_exams' object.pk %}">here</a> (you shouldn't) These should be edited via the exam candidate page. If you need to manually toggle you can do so <a href="{% url 'generic:update_cid_exams' object.pk %}">here</a> (you shouldn't)
</p> </p>
{% endblock %} {% endblock %}
@@ -6,7 +6,7 @@
<a href="{% url 'generic:user_group_create' %}">Create new group</a> <a href="{% url 'generic:user_group_create' %}">Create new group</a>
</p> </p>
{% for group in groups %} {% for group in groups %}
<p>{{group.name}} <p><a href="{% url 'generic:user_group_detail' group.pk %}"><h4>{{group.name}}</h4></a>
{% comment %} <button class="small-url-button" data-url="{% url 'generic:group_email' pk=group.pk %} {% comment %} <button class="small-url-button" data-url="{% url 'generic:group_email' pk=group.pk %}
">Email candidate details</button> ">Email candidate details</button>
<button class="small-url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %} <button class="small-url-button" data-url="{% url 'generic:group_email_resend' pk=group.pk %}
+43
View File
@@ -0,0 +1,43 @@
{% extends 'generic/base.html' %}
{% block content %}
<h2>Group: {{group.name}}</h2>
<h3>Users</h3>
<p>Count: {{users|length}}</p>
{% if users %}
<table>
<thead>
<tr><th>User</th></tr>
</thead>
{% for user in users %}
<tr><td>{{user.username}}</td></tr>
{% endfor %}
</table>
{% else %}
This group has no users.
{% endif %}
<h3>Exams</h3>
The group is currently associated with the following exams
{% with group.GetGroupExams as exam_map %}
{% for key, value in exam_map.items %}
<h4>{{key}}</h4>
<ul>
{% for exam in value %}
<li>
<a href="{{exam.get_absolute_url}}">{{exam}}</a>
</li>
{% endfor %}
</ul>
{% endfor %}
{% endwith %}
{% endblock %}
{% block js %}
<style>
td, th { padding-left: 10px }
</style>
{% endblock %}
+1
View File
@@ -78,6 +78,7 @@ urlpatterns = [
), ),
path("user/group/", views.user_group_view, name="user_group_view"), path("user/group/", views.user_group_view, name="user_group_view"),
path("user/group/all", views.user_group_view_all, name="user_group_view_all"), path("user/group/all", views.user_group_view_all, name="user_group_view_all"),
path("user/group/<int:group_id>/", views.user_group_view_detail, name="user_group_detail"),
path( path(
"user/group/<int:pk>/update", "user/group/<int:pk>/update",
views.UserUserGroupUpdate.as_view(), views.UserUserGroupUpdate.as_view(),
+21
View File
@@ -2141,6 +2141,17 @@ def cid_group_view_all(request):
{"groups": groups, "view_all": True}, {"groups": groups, "view_all": True},
) )
@user_is_cid_user_manager
def user_group_view_detail(request, group_id):
group = get_object_or_404(UserUserGroup, pk=group_id)
users = group.users.filter() # Filter inactive users?
return render(
request,
"generic/user_group_view_detail.html",
{"group": group, "users": users},
)
@user_is_cid_user_manager @user_is_cid_user_manager
def user_group_view(request): def user_group_view(request):
@@ -2493,6 +2504,11 @@ class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
print(context["view"].model.app_name) print(context["view"].model.app_name)
return context return context
def get_form_kwargs(self):
kwargs = super(ExamCreateBase, self).get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
class ExamUpdateBase(RevisionMixin, LoginRequiredMixin, UpdateView): class ExamUpdateBase(RevisionMixin, LoginRequiredMixin, UpdateView):
template_name = "exam_update_form.html" template_name = "exam_update_form.html"
@@ -2505,6 +2521,11 @@ class ExamUpdateBase(RevisionMixin, LoginRequiredMixin, UpdateView):
return super().form_valid(form) return super().form_valid(form)
def get_form_kwargs(self):
kwargs = super(ExamUpdateBase, self).get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
class ExamDeleteBase(RevisionMixin, DeleteView): class ExamDeleteBase(RevisionMixin, DeleteView):
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
+27 -21
View File
@@ -1,39 +1,45 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="anatomy"> <div class="anatomy">
<h1>Privacy Policy</h1> <h1>Privacy Policy</h1>
<h2>User information</h2>
<p>We aim to store the minimum information required to enable delivery of the required service on the platform. What is stored depends on the service being provided.</p> <p>We aim to store the minimum information required to enable delivery of the required service on the platform. What is stored depends on the service being provided.</p>
<h3>CID candidates</h3> <h3>CID candidates</h3>
<p>Candidates who receive a CID number may have the following information stored on the server.</p> <p>Candidates who receive a CID number may have the following information stored on the server.</p>
<ul> <ul>
<li>Name</li> <li>Name</li>
<li>Email address</li> <li>Email address</li>
</ul> </ul>
<p>These details will be linked (via a CID) with any answers given in the course of using the platform. <p>These details will be linked (via a CID) with any answers given in the course of using the platform.</p>
<h3>User accounts</h3> <h3>User accounts</h3>
<p>User accounts may have the following information stored on the server.</p> <p>User accounts may have the following information stored on the server.</p>
<ul> <ul>
<li>Name</li> <li>Name</li>
<li>Email address</li> <li>Email address</li>
<li>Supervisor</li> <li>Supervisor</li>
<li>Professional registration number</li> <li>Professional registration number</li>
<li>Grade</li> <li>Grade</li>
</ul> </ul>
<p>These details will be linked (via a the user account) with any answers given in the course of using the platform. <p>These details will be linked (via a the user account) with any answers given in the course of using the platform.</p>
<h3>Supervisors</h3> <h3>Supervisors</h3>
<p>A supervisor may have a user account in which case all of the above (under the "User accounts" section) may apply</p> <p>A supervisor may have a user account in which case all of the above (under the "User accounts" section) may apply</p>
<p>In addition the following information may be stored</p> <p>In addition the following information may be stored</p>
<ul> <ul>
<li>Name</li> <li>Name</li>
<li>Email address</li> <li>Email address</li>
<li>Trainee(s)</li> <li>Trainee(s)</li>
<li>Site</li> <li>Site</li>
</ul> </ul>
</div> <h2>Patient details</h2>
<p>No patient identifiable information is stored on the system. If you believe you this is incorrect please flag the approapriate item via the built in system.
</div>
{% endblock %} {% endblock %}
+25 -7
View File
@@ -24,15 +24,33 @@
</div> </div>
<h3>Groups</h3> <h3>Groups</h3>
<ul> <ul>
{% for group in user.user_groups.all %} {% for group in user.user_groups.all %}
<li>{{group}}</li> <li><a href="{% url 'generic:user_group_detail' group.pk %}">{{group}}</a></li>
{% empty %} {% empty %}
<li>No groups.</li> <li>No groups.</li>
{% endfor %} {% endfor %}
</ul> </ul>
<h3>Exams</h3>
<p>
{% with user.userprofile.get_exams as exam_map %}
{% for exam_type, exams in exam_map.items %}
<h4>{{exam_type}}</h4>
<ul>
{% for exam in exams %}
<li>
<a href="{{exam.get_absolute_url}}">{{exam}}</a>
</li>
{% endfor %}
</ul>
{% empty %}
No exams.
{% endfor %}
{% endwith %}
</p>
<a href="{% url 'password_change'%}">Change password</a> <a href="{% url 'password_change'%}">Change password</a>