From 747bf93373def5a06c967b5383dc1cce7f012570 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 28 Nov 2022 09:50:07 +0000 Subject: [PATCH] Complete supervisor model and start improving forms --- TODO.md | 4 + anatomy/static/css/anatomy.css | 32 +++++ .../templates/anatomy/exam_scores_user.html | 11 +- generic/forms.py | 81 ++++++++--- generic/migrations/0047_auto_20221121_1637.py | 29 ++++ generic/models.py | 12 +- generic/tables.py | 11 +- generic/templates/generic/cid_group_view.html | 2 +- generic/templates/generic/cid_view.html | 3 +- .../templates/generic/cidusergroup_form.html | 16 ++- generic/templates/generic/exam_cids_edit.html | 2 +- .../templates/generic/supervisor_form.html | 3 +- .../templates/generic/supervisor_list.html | 4 +- .../templates/generic/userusergroup_form.html | 22 +-- longs/templates/longs/exam_scores_user.html | 135 +++++++++--------- .../templates/physics/exam_scores_user.html | 33 ++--- rad/settings.py | 3 + rapids/templates/rapids/exam_scores_user.html | 12 +- requirements.txt | 3 +- sbas/templates/sbas/exam_scores_user.html | 12 +- templates/base.html | 1 + templates/index.html | 1 + templates/profile.html | 30 ++-- .../user_score_header.html | 12 +- templates/user_scores_footer.html | 11 ++ templates/user_update.html | 4 +- templates/user_update_profile.html | 3 +- 27 files changed, 309 insertions(+), 183 deletions(-) create mode 100644 generic/migrations/0047_auto_20221121_1637.py rename generic/templates/generic/user_scores_header.html => templates/user_score_header.html (54%) create mode 100644 templates/user_scores_footer.html diff --git a/TODO.md b/TODO.md index d6e4e4f3..4cc1856c 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,7 @@ - Improve documentation - Test coverage + + +# Future +- Clean up user results / score pages \ No newline at end of file diff --git a/anatomy/static/css/anatomy.css b/anatomy/static/css/anatomy.css index 5dabd15e..c6ab1581 100644 --- a/anatomy/static/css/anatomy.css +++ b/anatomy/static/css/anatomy.css @@ -453,6 +453,15 @@ td.user-answer-score-2.rapid-ans::after { padding-top: 10px; } +.submitted-user-answer:hover::after { + content: " Submitted answer"; + opacity: 50%; +} + +.question-stem-text { + font-weight: bolder; +} + .parent-help:hover .help-text { opacity: 50%; } @@ -870,4 +879,27 @@ tr:has(.errorlist){ .errorlist + input { background-color: red; color: darkblue; +} + +/* Form styling */ + +.control-group { + padding-top: 10px; +} + +.help-block { + float: right; + opacity: 25%; +} + +.control-group:hover .help-block{ + opacity: 100%; +} + +.control-group { + padding-top: 10px; +} + +.inline-form { + display: inline-grid; } \ No newline at end of file diff --git a/anatomy/templates/anatomy/exam_scores_user.html b/anatomy/templates/anatomy/exam_scores_user.html index 1dfb9382..274c85d0 100644 --- a/anatomy/templates/anatomy/exam_scores_user.html +++ b/anatomy/templates/anatomy/exam_scores_user.html @@ -13,9 +13,7 @@
-

Exam: {{ exam.name }}

-

Candidate: {{ cid|default_if_none:request.user.username }}

- Answers: + {% include 'user_score_header.html' %} -
Total mark: {{ total_score }} / {{max_score}} -
- {% if cid %} - Other exams - {% endif %} -
+ {% include 'user_scores_footer.html' %}
{% endblock %} {% block js %} diff --git a/generic/forms.py b/generic/forms.py index 6c107833..71ed4d1b 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -7,14 +7,22 @@ from django.forms import ( CharField, HiddenInput, EmailField, - IntegerField - + IntegerField, ) from django.forms import inlineformset_factory from atlas.models import CaseCollection from django.contrib.auth.models import User -from generic.models import CidUser, CidUserGroup, Examination, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup +from generic.models import ( + CidUser, + CidUserGroup, + Examination, + QuestionNote, + Supervisor, + UserGrades, + UserProfile, + UserUserGroup, +) from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, TextInput, Textarea @@ -50,6 +58,7 @@ from sbas.models import CidUserAnswer as SbasCidUserAnswer from sbas.models import Exam as SbasExam from sbas.models import Question as SbasQuestion + class ExamFormMixin: class Meta: fields = [ @@ -64,7 +73,7 @@ class ExamFormMixin: "archive", "cid_user_groups", "user_user_groups", - #"author", + # "author", ] def save(self, commit=True): @@ -75,6 +84,7 @@ class ExamFormMixin: return instance + class ExamAuthorFormMixin(ModelForm): class Meta: fields = ["author"] @@ -133,7 +143,6 @@ class QuestionNoteForm(ModelForm): class CidUserForm(ModelForm): - def __init__(self, *args, **kwargs): if kwargs.get("instance"): # We get the 'initial' keyword argument or initialize it @@ -141,12 +150,22 @@ class CidUserForm(ModelForm): initial = kwargs.setdefault("initial", {}) # The widget for a ModelMultipleChoiceField expects # a list of primary key for the selected data. - initial["physics_exams"] = [t.pk for t in kwargs["instance"].physics_exams.all()] - initial["anatomy_exams"] = [t.pk for t in kwargs["instance"].anatomy_exams.all()] + initial["physics_exams"] = [ + t.pk for t in kwargs["instance"].physics_exams.all() + ] + initial["anatomy_exams"] = [ + t.pk for t in kwargs["instance"].anatomy_exams.all() + ] initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()] - initial["rapid_exams"] = [t.pk for t in kwargs["instance"].rapid_exams.all()] - initial["longs_exams"] = [t.pk for t in kwargs["instance"].longs_exams.all()] - initial["casecollection_exams"] = [t.pk for t in kwargs["instance"].casecollection_exams.all()] + initial["rapid_exams"] = [ + t.pk for t in kwargs["instance"].rapid_exams.all() + ] + initial["longs_exams"] = [ + t.pk for t in kwargs["instance"].longs_exams.all() + ] + initial["casecollection_exams"] = [ + t.pk for t in kwargs["instance"].casecollection_exams.all() + ] ModelForm.__init__(self, *args, **kwargs) @@ -155,12 +174,16 @@ class CidUserForm(ModelForm): self.fields["physics_exams"] = ModelMultipleChoiceField( required=False, queryset=PhysicsExam.objects.filter(archive=False), - widget=FilteredSelectMultiple(verbose_name="Physics Exams", is_stacked=False), + widget=FilteredSelectMultiple( + verbose_name="Physics Exams", is_stacked=False + ), ) self.fields["anatomy_exams"] = ModelMultipleChoiceField( required=False, queryset=AnatomyExam.objects.filter(archive=False), - widget=FilteredSelectMultiple(verbose_name="Anatomy Exams", is_stacked=False), + widget=FilteredSelectMultiple( + verbose_name="Anatomy Exams", is_stacked=False + ), ) self.fields["sba_exams"] = ModelMultipleChoiceField( required=False, @@ -180,7 +203,9 @@ class CidUserForm(ModelForm): self.fields["casecollection_exams"] = ModelMultipleChoiceField( required=False, queryset=CaseCollection.objects.filter(archive=False), - widget=FilteredSelectMultiple(verbose_name="Case Collection", is_stacked=False), + widget=FilteredSelectMultiple( + verbose_name="Case Collection", is_stacked=False + ), ) def save(self, commit=True): @@ -219,7 +244,7 @@ class CidUserForm(ModelForm): self.save_m2m() return instance - + class Meta: model = CidUser fields = [ @@ -234,31 +259,42 @@ class CidUserForm(ModelForm): "group", ] + +class CidUserGroupModelChoiceField(ModelMultipleChoiceField): + def label_from_instance(self, obj): + return f"{obj.cid} [{obj.email}]" + + class CidUserGroupForm(ModelForm): + class Meta: model = CidUserGroup fields = ["name", "archive"] + class UserUserGroupModelChoiceField(ModelMultipleChoiceField): def label_from_instance(self, obj): return f"{obj.username} ({obj.userprofile.grade}) [{obj.email}]" + class UserUserGroupForm(ModelForm): users = UserUserGroupModelChoiceField( required=False, queryset=User.objects.all(), widget=FilteredSelectMultiple(verbose_name="Users", is_stacked=False), ) + class Meta: model = UserUserGroup fields = ["name", "archive", "users"] -#class UserForm(ModelForm): + +# class UserForm(ModelForm): # class Meta: # model = User # fields = ["first_name", "last_name", "email"] # -#class UserProfileForm(ModelForm): +# class UserProfileForm(ModelForm): # class Meta: # model = UserProfile # fields = ["supervisor_name", "supervisor_email", "registration_number", "peninsula_trainee"] @@ -271,18 +307,25 @@ GRADE_CHOICES = ( ("ST5", "ST5"), ) + class UserUserForm(Form): - username = EmailField(required=True, help_text="Username / email should be the same. Ideally this should be an nhs email.") + 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) # Needs to be a user/object ref + supervisor = ModelChoiceField( + Supervisor.objects.all(), required=False + ) # Needs to be a user/object ref # # class Meta: # # fields = ("username", "first_name", "last_name") + class SupervisorForm(ModelForm): class Meta: model = Supervisor - exclude = ("",) \ No newline at end of file + exclude = ("",) diff --git a/generic/migrations/0047_auto_20221121_1637.py b/generic/migrations/0047_auto_20221121_1637.py new file mode 100644 index 00000000..108252d3 --- /dev/null +++ b/generic/migrations/0047_auto_20221121_1637.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.13 on 2022-11-21 16:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0046_auto_20221121_0955'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisor', + name='email', + field=models.EmailField(help_text='The (nhs.net) email address of the supervisor', max_length=254, unique=True), + ), + migrations.AlterField( + model_name='supervisor', + name='name', + field=models.CharField(help_text='Name of the supervisor', max_length=255), + ), + migrations.AlterField( + model_name='supervisor', + name='site', + field=models.ForeignKey(blank=True, help_text='Hospital site at which the supervisor is based', null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.site'), + ), + ] diff --git a/generic/models.py b/generic/models.py index 1c1d6000..b9bab9ee 100644 --- a/generic/models.py +++ b/generic/models.py @@ -638,13 +638,13 @@ class CidUserGroup(models.Model): class UserUserGroup(models.Model): - name = models.CharField(blank=True, max_length=50) - archive = models.BooleanField(default=False) + name = models.CharField(blank=True, max_length=50, help_text="Name of the User Group") + archive = models.BooleanField(default=False, help_text="Archived groups remain on the test system but are not displayed by default") users = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, - related_name="user_groups" + related_name="user_groups", ) def __str__(self) -> str: @@ -686,10 +686,10 @@ class Supervisor(models.Model): This can be linked with a user account. """ - email = models.EmailField(blank=True, unique=True, help_text="The (nhs.net) email address of the supervisor") - name = models.CharField(max_length=255, blank=True, help_text="Name of the supervisor") + email = models.EmailField(unique=True, help_text="The (nhs.net) email address of the supervisor") + name = models.CharField(max_length=255, help_text="Name of the supervisor") user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True, help_text="If the supervisor has an account on the test system that can be associated here") - site = models.ForeignKey("Site", on_delete=models.SET_NULL, null=True, blank=True) + site = models.ForeignKey("Site", on_delete=models.SET_NULL, null=True, blank=True, help_text="Hospital site at which the supervisor is based") active = models.BooleanField(default=True) def __str__(self) -> str: diff --git a/generic/tables.py b/generic/tables.py index ef69aa58..3e300ca0 100644 --- a/generic/tables.py +++ b/generic/tables.py @@ -33,6 +33,7 @@ class CidUserTable(tables.Table): #sba_exams = tables.ManyToManyColumn(verbose_name="SBA Exams") #longs_exams = tables.ManyToManyColumn(verbose_name="Long Exams") #anatomy_exams = tables.ManyToManyColumn(verbose_name="Anatomy Exams") + group = tables.Column(linkify=True) selection = tables.CheckBoxColumn(accessor="pk", orderable=False) @@ -143,6 +144,8 @@ class UserUserTable(tables.Table): empty_values=(), linkify={"viewname": "account_update", "args":[A("username")]}, orderable=True, verbose_name="Edit", ) + user_groups = tables.ManyToManyColumn(verbose_name="Groups", linkify_item=True) + class Meta: model = User template_name = "django_tables2/bootstrap4.html" @@ -150,17 +153,19 @@ class UserUserTable(tables.Table): # "cid", #"userprofile", "userprofile.grade", + #"user_groups", #"userprofile.supervisor", # "email", ) - sequence = ("user", "userprofile.grade", "supervisor", "edit") + sequence = ("user", "userprofile.grade", "supervisor", "user_groups", "edit") def __init__(self, data=None, *args, **kwargs): super().__init__( data.prefetch_related( - "supervisor", "userprofile", - #"userprofile.grade", + "user_groups", + "userprofile__grade", + "supervisor", ), *args, **kwargs, diff --git a/generic/templates/generic/cid_group_view.html b/generic/templates/generic/cid_group_view.html index 60d5b7ea..27fccdf3 100755 --- a/generic/templates/generic/cid_group_view.html +++ b/generic/templates/generic/cid_group_view.html @@ -6,7 +6,7 @@ Create new group

{% for group in groups %} -

{{group.name}} +

{{group.name}}