Further candidate management improvements
This commit is contained in:
+2
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 %}
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user