Further candidate management improvements
This commit is contained in:
+2
-1
@@ -22,7 +22,7 @@ from .models import (
|
||||
QuestionType,
|
||||
Exam,
|
||||
)
|
||||
from generic.models import Examination
|
||||
from generic.models import CidUserGroup, Examination
|
||||
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.forms.widgets import RadioSelect, TextInput, Textarea
|
||||
@@ -204,6 +204,7 @@ class BodyPartForm(ModelForm):
|
||||
|
||||
|
||||
class ExamForm(ExamFormMixin, ModelForm):
|
||||
|
||||
class Meta(ExamFormMixin.Meta):
|
||||
model = Exam
|
||||
|
||||
|
||||
@@ -907,4 +907,8 @@ tr:has(.errorlist){
|
||||
form .submit-button {
|
||||
margin-top: 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:
|
||||
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:
|
||||
fields = [
|
||||
"name",
|
||||
|
||||
+46
-10
@@ -422,16 +422,16 @@ class QuestionNote(models.Model):
|
||||
def get_short_str(self):
|
||||
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):
|
||||
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)
|
||||
passcode = models.CharField(blank=True, max_length=50)
|
||||
@@ -463,13 +463,13 @@ class CidUser(models.Model):
|
||||
def get_cid_exams(self):
|
||||
available_exams = []
|
||||
|
||||
for n, t in self.EXAM_TYPES:
|
||||
for n, t in EXAM_TYPES:
|
||||
exam_rel = getattr(self, t)
|
||||
if exam_rel.exists():
|
||||
exams = exam_rel.filter(exam_mode=True, archive=False).order_by("name")
|
||||
available_exams.append((n, exams))
|
||||
|
||||
return available_exams
|
||||
return dict(available_exams)
|
||||
|
||||
def generate_exam_report(self):
|
||||
exam_list = self.get_cid_exams()
|
||||
@@ -655,6 +655,13 @@ class CidUserGroup(models.Model):
|
||||
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):
|
||||
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])
|
||||
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):
|
||||
return reverse("generic:user_group_update", kwargs={"pk": self.pk})
|
||||
|
||||
@@ -684,6 +700,15 @@ class UserGrades(models.Model):
|
||||
def __str__(self):
|
||||
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):
|
||||
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")
|
||||
@@ -700,6 +725,17 @@ class UserProfile(models.Model):
|
||||
|
||||
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):
|
||||
"""Model to hold individual supervisor details (email and name)
|
||||
|
||||
|
||||
@@ -6,33 +6,46 @@
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
{{form.media}}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
{{form.media}}
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
|
||||
<!-- {{ form.media }} -->
|
||||
<!-- {{ form.media }} -->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Edit CID User / {{ciduser.cid}}</h2>
|
||||
Use this form to edit a CID user.
|
||||
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
|
||||
{% csrf_token %}
|
||||
<h2>Edit CID User / {{ciduser.cid}}</h2>
|
||||
Use this form to edit a CID user.
|
||||
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|crispy }}
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
|
||||
<h2>Exams</h2>
|
||||
<p>The user is currently part of the following exams</p>
|
||||
<h2>Exams</h2>
|
||||
<p>The user is currently part of the following exams</p>
|
||||
|
||||
<p>
|
||||
{{ object.get_cid_exams }}
|
||||
</p>
|
||||
<p>
|
||||
{% with object.get_cid_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>
|
||||
|
||||
<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)
|
||||
</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)
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,7 @@
|
||||
<a href="{% url 'generic:user_group_create' %}">Create new group</a>
|
||||
</p>
|
||||
{% 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 %}
|
||||
">Email candidate details</button>
|
||||
<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/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(
|
||||
"user/group/<int:pk>/update",
|
||||
views.UserUserGroupUpdate.as_view(),
|
||||
|
||||
@@ -2141,6 +2141,17 @@ def cid_group_view_all(request):
|
||||
{"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
|
||||
def user_group_view(request):
|
||||
@@ -2493,6 +2504,11 @@ class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
print(context["view"].model.app_name)
|
||||
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):
|
||||
template_name = "exam_update_form.html"
|
||||
@@ -2505,6 +2521,11 @@ class ExamUpdateBase(RevisionMixin, LoginRequiredMixin, UpdateView):
|
||||
|
||||
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):
|
||||
template_name = "exam_confirm_delete.html"
|
||||
|
||||
+27
-21
@@ -1,39 +1,45 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="anatomy">
|
||||
<div class="anatomy">
|
||||
<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>
|
||||
|
||||
<h3>CID candidates</h3>
|
||||
<p>Candidates who receive a CID number may have the following information stored on the server.</p>
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
</ul>
|
||||
<p>These details will be linked (via a CID) with any answers given in the course of using the platform.
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
</ul>
|
||||
<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>
|
||||
<p>User accounts may have the following information stored on the server.</p>
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
<li>Supervisor</li>
|
||||
<li>Professional registration number</li>
|
||||
<li>Grade</li>
|
||||
</ul>
|
||||
<p>These details will be linked (via a the user account) with any answers given in the course of using the platform.
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
<li>Supervisor</li>
|
||||
<li>Professional registration number</li>
|
||||
<li>Grade</li>
|
||||
</ul>
|
||||
<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>
|
||||
<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>
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
<li>Trainee(s)</li>
|
||||
<li>Site</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>Name</li>
|
||||
<li>Email address</li>
|
||||
<li>Trainee(s)</li>
|
||||
<li>Site</li>
|
||||
</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 %}
|
||||
+25
-7
@@ -24,15 +24,33 @@
|
||||
</div>
|
||||
|
||||
|
||||
<h3>Groups</h3>
|
||||
<h3>Groups</h3>
|
||||
|
||||
<ul>
|
||||
{% for group in user.user_groups.all %}
|
||||
<li>{{group}}</li>
|
||||
{% empty %}
|
||||
<ul>
|
||||
{% for group in user.user_groups.all %}
|
||||
<li><a href="{% url 'generic:user_group_detail' group.pk %}">{{group}}</a></li>
|
||||
{% empty %}
|
||||
<li>No groups.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user