Compare commits

...
4 Commits
Author SHA1 Message Date
Ross 80412dcbe0 improve trainee creation 2024-09-02 14:14:57 +01:00
Ross a024d7fca7 start cleaning up trainee management 2024-09-02 12:58:05 +01:00
Ross 33b49c9430 change user scores html base 2024-09-02 12:34:32 +01:00
Ross 4f60440ea1 allow adding users to exams by group 2024-09-02 12:16:37 +01:00
15 changed files with 269 additions and 81 deletions
@@ -1,4 +1,4 @@
{% extends 'anatomy/base.html' %}
{% extends 'base.html' %}
{% block content %}
<div class="question-display-block">
+66 -50
View File
@@ -68,6 +68,9 @@ from django.forms.utils import from_current_timezone, to_current_timezone
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from dal import autocomplete
class SplitDateTimeFieldDefaultTime(SplitDateTimeField):
def compress(self, data_list):
if data_list:
@@ -79,9 +82,9 @@ class SplitDateTimeFieldDefaultTime(SplitDateTimeField):
)
if data_list[1] in self.empty_values:
data_list[1] = datetime.time(0, 1)
#raise ValidationError(
# raise ValidationError(
# self.error_messages["invalid_time"], code="invalid_time"
#)
# )
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
@@ -98,9 +101,9 @@ class SplitDateTimeFieldDefaultTimeEnd(SplitDateTimeField):
)
if data_list[1] in self.empty_values:
data_list[1] = datetime.time(23, 59)
#raise ValidationError(
# raise ValidationError(
# self.error_messages["invalid_time"], code="invalid_time"
#)
# )
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
@@ -111,29 +114,28 @@ class ExamFormMixin:
super(ModelForm, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'id-exam-form'
self.helper.form_class = 'exam-form'
self.helper.form_method = 'post'
self.helper.form_action = 'submit'
self.helper.form_id = "id-exam-form"
self.helper.form_class = "exam-form"
self.helper.form_method = "post"
self.helper.form_action = "submit"
self.helper.add_input(Submit("submit", "Submit"))
self.helper.add_input(Submit('submit', 'Submit'))
#if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
# if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
# cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
# user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
#else:
# else:
# cid_user_group_queryset = CidUserGroup.objects.none()
# user_user_group_queryset = UserUserGroup.objects.none()
#self.fields["cid_user_groups"] = ModelMultipleChoiceField(
# self.fields["cid_user_groups"] = ModelMultipleChoiceField(
# required=False,
# queryset=cid_user_group_queryset,
#)
#self.fields["user_user_groups"] = ModelMultipleChoiceField(
# )
# self.fields["user_user_groups"] = ModelMultipleChoiceField(
# required=False,
# queryset=user_user_group_queryset,
#)
# )
self.fields["start_date"] = SplitDateTimeFieldDefaultTime(
widget=SplitDateTimeWidget(
date_attrs={"type": "date", "class": "datepicker"},
@@ -169,8 +171,8 @@ class ExamFormMixin:
"active",
"publish_results",
"archive",
#"cid_user_groups",
#"user_user_groups",
# "cid_user_groups",
# "user_user_groups",
# "author",
]
@@ -199,6 +201,7 @@ class ExamAuthorFormMixin(ModelForm):
widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False),
)
class ExamMarkerFormMixin(ModelForm):
class Meta:
fields = ["markers"]
@@ -213,37 +216,41 @@ class ExamMarkerFormMixin(ModelForm):
class ExamGroupsFormMixin(ModelForm):
class Meta:
#model = ExamCollection
fields = ("cid_user_groups", "user_user_groups")
class Meta:
# model = ExamCollection
fields = ("cid_user_groups", "user_user_groups")
def __init__(self, *args, user, **kwargs) -> None:
super(ModelForm, self).__init__(*args, **kwargs)
def __init__(self, *args, user, **kwargs) -> None:
super(ModelForm, self).__init__(*args, **kwargs)
# TODO: decide how we should handle managing user groups
# i.e who should be able to access a list of all users?
# TODO: decide how we should handle managing user groups
# i.e who should be able to access a list of all users?
if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
else:
cid_user_group_queryset = CidUserGroup.objects.filter(archive=False, open_access=True)
user_user_group_queryset = UserUserGroup.objects.none(archive=False, open_access=True)
#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,
label="CID User Groups",
if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
else:
cid_user_group_queryset = CidUserGroup.objects.filter(
archive=False, open_access=True
)
self.fields["user_user_groups"] = ModelMultipleChoiceField(
required=False,
queryset=user_user_group_queryset,
label="User Groups",
user_user_group_queryset = UserUserGroup.objects.none(
archive=False, open_access=True
)
self.fields["cid_user_groups"].widget.attrs["size"] = 8
self.fields["user_user_groups"].widget.attrs["size"] = 8
# 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,
label="CID User Groups",
)
self.fields["user_user_groups"] = ModelMultipleChoiceField(
required=False,
queryset=user_user_group_queryset,
label="User Groups",
)
self.fields["cid_user_groups"].widget.attrs["size"] = 8
self.fields["user_user_groups"].widget.attrs["size"] = 8
class ExaminationForm(ModelForm):
@@ -667,13 +674,23 @@ class UserUserForm(Form):
first_name = CharField(max_length=255, required=True)
last_name = CharField(max_length=255, required=True)
grade = ModelChoiceField(UserGrades.objects.all(), required=False)
class TraineeForm(Form):
username = EmailField(
required=True,
help_text="Username / email should be the same. Ideally this should be an nhs email.",
)
first_name = CharField(max_length=255, required=True)
last_name = CharField(max_length=255, required=True)
grade = ModelChoiceField(UserGrades.objects.all(), required=False)
supervisor = ModelChoiceField(
Supervisor.objects.all(), required=False
Supervisor.objects.all(), required=False, widget=autocomplete.ModelSelect2(url='generic:supervisor-autocomplete')
) # Needs to be a user/object ref
#
# class Meta:
#
# fields = ("username", "first_name", "last_name")
#class Meta:
# widgets = {
# "supervisor" : autocomplete.ModelSelect2(url='generic:supervisor-autocomplete')
# }
class SupervisorForm(ModelForm):
@@ -751,4 +768,3 @@ class ExamCollectionCloneForm(ModelForm):
class Meta:
model = ExamCollection
fields = ("name", "date")
@@ -39,44 +39,67 @@
{% endif %}
{% endif %}
<h3>Exams</h3>
The group is currently associated with the following exams.
The group is currently associated with the following exams. This allows the users within the group to be easily added to the exam. It will not automatically give members of the group access to the exam.
{% if group_type == "cid" %}
Add more <a href='{% url "generic:cid_group_exams" group.pk %}'>here</a>.
{% else %}
Add more <a href='{% url "generic:user_group_exams" group.pk %}'>here</a>.
{% endif %}
<p>It is possible to quickly add or remove all group candidates from an exam by clicking the buttons below. This will only affect the <b>{{users|length}}</b> candidates in this group. You will need to refresh the page to see updated candidate counts.</p>
{% with group.GetGroupExams as exam_map %}
{% for key, value in exam_map.items %}
<h4>{{key}}</h4>
<ul>
<div>
{% for exam in value %}
<li>
<a href="{{exam.get_absolute_url}}">{{exam}}</a>
<div class="d-flex">
<a href="{{exam.get_absolute_url}}" class="px-2">{{exam}}</a>
{% if group_type == "cid" %}
<a href="{{exam.get_cid_edit_url}}" class="edit-link">Edit candidates</a>
<a href="{{exam.get_cid_edit_url}}" class="edit-link" px-2>Edit candidates</a>
{% comment %} <button>Add group candidates</button>
<button>Remove group candidates</button> {% endcomment %}
{% else %}
<a href="{{exam.get_user_edit_url}}" class="edit-link">Edit candidates</a>
<a href="{{exam.get_user_edit_url}}" class="edit-link px-2">Edit candidates</a>
<form>
<input type="hidden" name="exam_id" value="{{exam.pk}}">
<input type="hidden" name="exam_type" value="{{exam.app_name}}">
<button
class="btn btn-sm px-2"
hx-post="{% url 'generic:user_group_add_candidates_to_exams' group.pk %}"
title="Add all group candidates to this exam"
name="action"
value="add"
>Add group candidates</button>
<button
hx-post="{% url 'generic:user_group_add_candidates_to_exams' group.pk %}"
title="Remove all group candidates to this exam"
class="btn btn-sm px-2"
name="action"
value="remove"
>Remove group candidates</button>
</form>
{% endif %}
<span class="cid-number">
<div class="px-2 ms-auto ">
{% if group_type == "cid" %}
(Cid candidate count: {{exam.valid_cid_users.count}})
{% else %}
(User candidate count: {{exam.valid_user_users.count}})
{% endif %}
</span>
</li>
</div>
</div>
{% endfor %}
</ul>
</div>
{% endfor %}
{% endwith %}
{% endblock %}
{% block js %}
<style>
td, th { padding-left: 10px }
.cid-number {float: right}
.edit-link {
font-size: smaller;
opacity: 60%;
+30
View File
@@ -0,0 +1,30 @@
{% extends "generic/base.html" %}
<!-- {% load static from static %} -->
{% load crispy_forms_tags %}
{% block css %}
{% endblock %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}}
<script type="text/javascript">
</script>
<!-- {{ form.media }} -->
{% endblock %}
{% block content %}
<h2>Create Trainee</h2>
Use this form to create a user. Only existing supervisors can be added (<a href="{% url 'generic:supervisor_create' %}">create them first</a> and refresh this page or add them later if they do not exist).
{% if errors %}
<div class="alert alert-info" role="alert">{{errors}}</a></div>
{% endif %}
<form action="" method="post" enctype="multipart/form-data" id="condition-form">
{% csrf_token %}
<table>
{{ form|crispy }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
</form>
{% endblock %}
+7 -1
View File
@@ -9,6 +9,7 @@
{% for i in "123456"|make_list %}
<a href='{% url "trainees_grade" "ST"|add:i %}'>ST{{i}}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
(<a href="{% url 'create_trainee' %}" title="Click to add a trainee to the platform, creating an account for them.">Add trainee</a>)
<h2>
{% if grade %}
@@ -38,7 +39,12 @@
<a href="{% url 'account_profile_update' trainee.user.username %}" class="add-supervisor">add</a>
{% endif %}
</td>
<td><a href="{% url 'account_update' trainee.user.username %}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}">Profile</a></td>
<td><a href="{% url 'account_update' trainee.user.username %}">User</a>/<a href="{% url 'account_profile_update' trainee.user.username %}">Profile</a>
<span class="hover-highlight"><i class="bi bi-x-circle" title="Click to remove trainee status"
hx-get = "{% url 'generic:user_not_trainee' trainee.user.pk %}"
></i></span>
</td>
</tr>
{% endfor %}
@@ -1,5 +1,6 @@
{% extends "generic/base.html" %}
<!-- {% load static from static %} -->
{% load crispy_forms_tags %}
{% block css %}
{% endblock %}
@@ -13,8 +14,8 @@
<!-- {{ form.media }} -->
{% endblock %}
{% block content %}
<h2>Edit User / {{ciduser.cid}}</h2>
Use this form to create a user. Only existing supervisors can be added (<a href="{% url 'generic:supervisor_create' %}">create them first</a> and refresh this page or add them later if they do not exist).
<h2>Create User</h2>
Use this form to create a user. Create a trainee <a href="{% url 'create_trainee' %}">here</a>.
{% if errors %}
<div class="alert alert-info" role="alert">{{errors}}</a></div>
{% endif %}
@@ -22,7 +23,7 @@ Use this form to create a user. Only existing supervisors can be added (<a href=
{% csrf_token %}
<table>
{{ form.as_table }}
{{ form|crispy }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
</form>
+23 -1
View File
@@ -1,6 +1,6 @@
from django.urls import path, include
from generic.models import Examination
from generic.models import Examination, Supervisor
from . import views
from generic.views import ExamViews as GenericExamViews, GenericViewBase
@@ -42,6 +42,13 @@ urlpatterns = [
),
name="examination-autocomplete",
),
path(
"supervisor-autocomplete",
views.SupervisorAutocomplete.as_view(
model=Supervisor
),
name="supervisor-autocomplete",
),
path(
"cids/manage/<int:pk>/update", views.CidUserUpdate.as_view(), name="update_cid"
),
@@ -113,6 +120,11 @@ urlpatterns = [
views.users_bulk_edit,
name="users_bulk_edit",
),
path(
"user/<int:user_id>/not_trainee",
views.user_not_trainee,
name="user_not_trainee",
),
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(
@@ -120,6 +132,16 @@ urlpatterns = [
views.user_group_view_detail,
name="user_group_detail",
),
path(
"user/group/<int:group_id>/add",
views.user_group_add_candidates_to_exams,
name="user_group_add_candidates_to_exams",
),
path(
"user/group/<int:group_id>/",
views.user_group_view_detail,
name="user_group_detail",
),
path(
"user/group/<int:pk>/exams",
views.UserGroupExamUpdate.as_view(),
+85 -9
View File
@@ -53,7 +53,7 @@ from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
import zipfile
from django.core.files.base import ContentFile
from django.db.models import Q
from .forms import (
CidGroupExamForm,
@@ -65,6 +65,7 @@ from .forms import (
CidUserGroupForm,
ExaminationMergeForm,
SupervisorForm,
TraineeForm,
UserGroupExamForm,
CidGroupExamForm,
UserUserForm,
@@ -133,6 +134,17 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
# return obj
# raise PermissionDenied() # or Http404
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
EXAM_MAP = {
"physics": PhysicsExam,
"anatomy": AnatomyExam,
"rapids": RapidsExam,
"longs": LongsExam,
"sbas": SbasExam,
}
return EXAM_MAP[app_name]
def normaliseRapidsScore(score):
if score == 49:
@@ -2597,6 +2609,23 @@ class ExaminationAutocomplete(autocomplete.Select2QuerySetView):
return Examination.objects.none()
return qs
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# TODO: we should probably filter this to only
# allow access by trainees / other suprevisors
if not self.request.user.is_authenticated:
return Supervisor.objects.none()
qs = Supervisor.objects.all()
if self.q:
# This raises a fielderror which breaks creating a new item if not caught
try:
qs = qs.filter(Q(email__icontains=self.q) | Q(name__icontains=self.q))
except FieldError:
return Supervisor.objects.none()
return qs
class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
@@ -2709,6 +2738,30 @@ def cid_group_view_all(request):
{"groups": groups, "view_all": True},
)
@user_is_cid_user_manager
def user_group_add_candidates_to_exams(request, group_id):
if request.htmx:
group = get_object_or_404(UserUserGroup, pk=group_id)
exam_id = request.POST.get("exam_id")
exam_type = request.POST.get("exam_type")
ExamModel = get_exam_model_from_app_name(exam_type)
exam = get_object_or_404(ExamModel, pk=exam_id)
if request.POST.get("action") == "remove":
exam.valid_user_users.remove(*group.users.all())
return HttpResponse(f"Candidates removed")
else:
exam.valid_user_users.add(*group.users.all())
return HttpResponse(f"Candidates added")
#exam.save()
raise PermissionDenied() # or Http404
pass
@user_is_cid_user_manager
def user_group_view_detail(request, group_id):
@@ -2756,6 +2809,14 @@ def get_user_selection_from_request(request):
user_models = User.objects.filter(id__in=selected_users)
return user_models
@user_is_cid_user_manager
def user_not_trainee(request, user_id):
user = get_object_or_404(User, pk=user_id)
user.userprofile.peninsula_trainee = False
user.userprofile.save()
return HttpResponse(f"{user.username} is no longer a trainee", content_type="text/plain")
@user_is_cid_user_manager
def users_bulk_edit(request):
@@ -3321,12 +3382,24 @@ def trainees(request, grade: None|str=None):
return render(request, "generic/trainees.html", context)
def create_user(request, context=None):
def create_trainee(request, context=None):
return create_user(request, context, trainee=True)
@user_is_cid_user_manager
def create_user(request, context=None, trainee: bool=False):
if trainee:
form_template = "generic/trainee_creation_form.html"
else:
form_template = "generic/user_creation_form.html"
# if this is a POST request we need to process the form data
if request.method == "POST":
# create a form instance and populate it with data from the request:
form = UserUserForm(request.POST)
if trainee:
form = TraineeForm(request.POST)
else:
form = UserUserForm(request.POST)
# check whether it's valid:
if form.is_valid():
try:
user_dict = {
@@ -3342,19 +3415,19 @@ def create_user(request, context=None):
errors = f"Unable to create account: {error}"
return render(
request,
"generic/user_creation_form.html",
form_template,
{"form": form, "errors": errors},
)
try:
user_profile = UserProfile.objects.get(user=new_user)
user_profile.peninsula_trainee = True
user_profile.peninsula_trainee = trainee
if request.POST["grade"]:
grade = UserGrades.objects.get(pk=request.POST["grade"])
user_profile.grade = grade
if request.POST["supervisor"]:
if "supervisor" in request.POST and request.POST["supervisor"]:
supervisor = Supervisor.objects.get(pk=request.POST["supervisor"])
user_profile.supervisor = supervisor
@@ -3364,7 +3437,7 @@ def create_user(request, context=None):
errors = f"Unable to create account profile {error}"
return render(
request,
"generic/user_creation_form.html",
form_template,
{"form": form, "errors": errors},
)
@@ -3373,9 +3446,12 @@ def create_user(request, context=None):
# if a GET (or any other method) we'll create a blank form
else:
form = UserUserForm()
if trainee:
form = TraineeForm()
else:
form = UserUserForm()
return render(request, "generic/user_creation_form.html", {"form": form})
return render(request, form_template, {"form": form})
class SupervisorDetail(CidManagerRequiredMixin, DetailView):
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'longs/base.html' %}
{% extends 'base.html' %}
{% block content %}
<div class="question-display-block">
@@ -1,4 +1,4 @@
{% extends 'physics/base.html' %}
{% extends 'base.html' %}
{% block content %}
<div class="physics">
+8
View File
@@ -1275,3 +1275,11 @@ tr:has(> td > a) {
color: lightgray;
overflow: auto;
}
.hover-highlight {
opacity: 50%;
}
.hover-highlight:hover {
opacity: 100%;
color:purple;
}
+3
View File
@@ -75,6 +75,9 @@ urlpatterns = [
path(
"accounts/create/", generic_views.create_user, name="create_user"
),
path(
"accounts/trainees/create/", generic_views.create_trainee, name="create_trainee"
),
path(
"accounts/trainees/", generic_views.trainees, name="trainees"
),
@@ -1,4 +1,4 @@
{% extends 'rapids/base.html' %}
{% extends 'base.html' %}
{% block content %}
<div class="question-display-block">
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends 'sbas/base.html' %}
{% extends 'base.html' %}
{% block content %}
<div class="sbas">
+3
View File
@@ -7,7 +7,10 @@
</div>
{% endif %}
<div>
<br/>
{% if cid %}
<a href="{% url 'cid_scores' cid passcode %}">Other exams</a>
{% else %}
<a href="{% url 'user_scores' %}">Other exams</a>
{% endif %}
</div>