From 6e6ba178b5e87823d420fa88a92b37a61a015b70 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 14 Nov 2022 14:25:24 +0000 Subject: [PATCH] Improvements to user management --- TODO.md | 8 ++ .../0067_alter_anatomyquestion_image.py | 19 ++++ anatomy/static/css/anatomy.css | 12 +++ atlas/templates/atlas/finding_detail.html | 2 - generic/admin.py | 5 +- generic/forms.py | 23 ++++- generic/migrations/0037_supervisor.py | 26 ++++++ .../migrations/0038_alter_supervisor_user.py | 21 +++++ .../migrations/0039_alter_supervisor_user.py | 21 +++++ .../migrations/0040_alter_supervisor_email.py | 18 ++++ generic/migrations/0041_auto_20221114_1355.py | 54 +++++++++++ .../migrations/0042_userprofile_supervisor.py | 19 ++++ .../0043_alter_userprofile_supervisor.py | 19 ++++ generic/models.py | 33 ++++++- generic/templates/generic/base.html | 1 + .../templates/generic/exam_scores_base.html | 4 +- .../templates/generic/supervisor_base.html | 12 +++ .../templates/generic/supervisor_detail.html | 27 ++++++ .../templates/generic/supervisor_form.html | 32 +++++++ .../templates/generic/supervisor_list.html | 20 +++++ .../templates/generic/user_creation_form.html | 29 ++++++ generic/urls.py | 5 ++ generic/views.py | 89 ++++++++++++++++++- rad/urls.py | 5 ++ rad/views.py | 1 + templates/profile.html | 5 +- templates/user_list_view.html | 25 ++++-- 27 files changed, 515 insertions(+), 20 deletions(-) create mode 100644 TODO.md create mode 100644 anatomy/migrations/0067_alter_anatomyquestion_image.py create mode 100644 generic/migrations/0037_supervisor.py create mode 100644 generic/migrations/0038_alter_supervisor_user.py create mode 100644 generic/migrations/0039_alter_supervisor_user.py create mode 100644 generic/migrations/0040_alter_supervisor_email.py create mode 100644 generic/migrations/0041_auto_20221114_1355.py create mode 100644 generic/migrations/0042_userprofile_supervisor.py create mode 100644 generic/migrations/0043_alter_userprofile_supervisor.py create mode 100755 generic/templates/generic/supervisor_base.html create mode 100755 generic/templates/generic/supervisor_detail.html create mode 100644 generic/templates/generic/supervisor_form.html create mode 100755 generic/templates/generic/supervisor_list.html create mode 100755 generic/templates/generic/user_creation_form.html diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..d6e4e4f3 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# Current target +- User management + + +# Ongoing + +- Improve documentation +- Test coverage diff --git a/anatomy/migrations/0067_alter_anatomyquestion_image.py b/anatomy/migrations/0067_alter_anatomyquestion_image.py new file mode 100644 index 00000000..0d618c6a --- /dev/null +++ b/anatomy/migrations/0067_alter_anatomyquestion_image.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-11-14 09:44 + +import anatomy.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anatomy', '0066_remove_exam_stats_graph'), + ] + + operations = [ + migrations.AlterField( + model_name='anatomyquestion', + name='image', + field=models.ImageField(help_text="The image to use for the question. Ideally use use unmarked images and annotate (arrow) them on the test system. If you wish to reuse an image that is already uploaded 'clone' the question that contains it.", upload_to=anatomy.models.image_directory_path), + ), + ] diff --git a/anatomy/static/css/anatomy.css b/anatomy/static/css/anatomy.css index b8f7971c..5dabd15e 100644 --- a/anatomy/static/css/anatomy.css +++ b/anatomy/static/css/anatomy.css @@ -858,4 +858,16 @@ details.filter { border: 1px solid gray; padding: 10px; margin: 20px; +} + +/* Generic default styling for forms */ + +/* Highlight form errors */ +tr:has(.errorlist){ + border: 1px dashed red; +} + +.errorlist + input { + background-color: red; + color: darkblue; } \ No newline at end of file diff --git a/atlas/templates/atlas/finding_detail.html b/atlas/templates/atlas/finding_detail.html index 29b9b3ab..5f82131c 100755 --- a/atlas/templates/atlas/finding_detail.html +++ b/atlas/templates/atlas/finding_detail.html @@ -2,8 +2,6 @@ {% block content %} - -
Edit Delete diff --git a/generic/admin.py b/generic/admin.py index cba9b4b1..c673ed6b 100644 --- a/generic/admin.py +++ b/generic/admin.py @@ -15,13 +15,15 @@ from .models import ( CidUserExam, UserGrades, UserProfile, + Supervisor, + Site ) # from .models import Examination, Sign, Site, Condition admin.site.register(Examination) admin.site.register(Sign) -# admin.site.register(Site) +admin.site.register(Site) admin.site.register(Condition) admin.site.register(Plane) admin.site.register(Contrast) @@ -32,3 +34,4 @@ admin.site.register(CidUserExam) admin.site.register(CidUserGroup) admin.site.register(UserProfile) admin.site.register(UserGrades) +admin.site.register(Supervisor) diff --git a/generic/forms.py b/generic/forms.py index 470f1df0..df506633 100755 --- a/generic/forms.py +++ b/generic/forms.py @@ -6,12 +6,15 @@ from django.forms import ( ChoiceField, CharField, HiddenInput, + EmailField, + 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, UserProfile, UserUserGroup +from generic.models import CidUser, CidUserGroup, Examination, QuestionNote, Supervisor, UserProfile, UserUserGroup from django.contrib.admin.widgets import FilteredSelectMultiple from django.forms.widgets import RadioSelect, TextInput, Textarea @@ -258,4 +261,20 @@ class UserUserGroupForm(ModelForm): #class UserProfileForm(ModelForm): # class Meta: # model = UserProfile -# fields = ["supervisor_name", "supervisor_email", "registration_number", "peninsula_trainee"] \ No newline at end of file +# fields = ["supervisor_name", "supervisor_email", "registration_number", "peninsula_trainee"] + +class UserUserForm(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 = IntegerField(min_value=1, max_value=6, required=False) + supervisor = CharField(max_length=255, 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 diff --git a/generic/migrations/0037_supervisor.py b/generic/migrations/0037_supervisor.py new file mode 100644 index 00000000..41b341be --- /dev/null +++ b/generic/migrations/0037_supervisor.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.13 on 2022-11-14 09:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('generic', '0036_ciduserexam_results_emailed_status'), + ] + + operations = [ + migrations.CreateModel( + name='Supervisor', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(blank=True, max_length=254)), + ('name', models.CharField(blank=True, max_length=255)), + ('active', models.BooleanField(default=True)), + ('user', models.OneToOneField(blank=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/generic/migrations/0038_alter_supervisor_user.py b/generic/migrations/0038_alter_supervisor_user.py new file mode 100644 index 00000000..bdd48489 --- /dev/null +++ b/generic/migrations/0038_alter_supervisor_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-11-14 13:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('generic', '0037_supervisor'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisor', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/generic/migrations/0039_alter_supervisor_user.py b/generic/migrations/0039_alter_supervisor_user.py new file mode 100644 index 00000000..cd8a21d7 --- /dev/null +++ b/generic/migrations/0039_alter_supervisor_user.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.13 on 2022-11-14 13:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('generic', '0038_alter_supervisor_user'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisor', + name='user', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/generic/migrations/0040_alter_supervisor_email.py b/generic/migrations/0040_alter_supervisor_email.py new file mode 100644 index 00000000..6bb696bb --- /dev/null +++ b/generic/migrations/0040_alter_supervisor_email.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.13 on 2022-11-14 13:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0039_alter_supervisor_user'), + ] + + operations = [ + migrations.AlterField( + model_name='supervisor', + name='email', + field=models.EmailField(blank=True, max_length=254, unique=True), + ), + ] diff --git a/generic/migrations/0041_auto_20221114_1355.py b/generic/migrations/0041_auto_20221114_1355.py new file mode 100644 index 00000000..58d4b5bb --- /dev/null +++ b/generic/migrations/0041_auto_20221114_1355.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2.13 on 2022-11-14 13:55 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('generic', '0040_alter_supervisor_email'), + ] + + operations = [ + migrations.RemoveField( + model_name='site', + name='initials', + ), + migrations.RemoveField( + model_name='site', + name='site', + ), + migrations.AddField( + model_name='site', + name='full_name', + field=models.CharField(blank=True, help_text='Name of the site', max_length=255), + ), + migrations.AddField( + model_name='site', + name='short_code', + field=models.CharField(blank=True, help_text='Shortcode/name of the site', max_length=255), + ), + migrations.AddField( + model_name='supervisor', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.site'), + ), + migrations.AlterField( + model_name='supervisor', + name='email', + field=models.EmailField(blank=True, 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(blank=True, help_text='Name of the supervisor', max_length=255), + ), + migrations.AlterField( + model_name='supervisor', + name='user', + field=models.OneToOneField(blank=True, help_text='If the supervisor has an account on the test system that can be associated here', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/generic/migrations/0042_userprofile_supervisor.py b/generic/migrations/0042_userprofile_supervisor.py new file mode 100644 index 00000000..c9e48dc5 --- /dev/null +++ b/generic/migrations/0042_userprofile_supervisor.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-11-14 14:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0041_auto_20221114_1355'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.supervisor'), + ), + ] diff --git a/generic/migrations/0043_alter_userprofile_supervisor.py b/generic/migrations/0043_alter_userprofile_supervisor.py new file mode 100644 index 00000000..a57b60af --- /dev/null +++ b/generic/migrations/0043_alter_userprofile_supervisor.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.13 on 2022-11-14 14:07 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0042_userprofile_supervisor'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='supervisor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='trainee', to='generic.supervisor'), + ), + ] diff --git a/generic/models.py b/generic/models.py index 6d6c95b7..e178f704 100644 --- a/generic/models.py +++ b/generic/models.py @@ -56,11 +56,13 @@ class Examination(models.Model): class Site(models.Model): - site = models.CharField(max_length=200) - initials = models.CharField(max_length=200) + """Model to hold site details""" + full_name = models.CharField(max_length=255, blank=True, help_text="Name of the site") + short_code = models.CharField(max_length=255, blank=True, help_text="Shortcode/name of the site") + def __str__(self): - return self.site + return self.short_code class Condition(tagulous.models.TagModel): @@ -667,6 +669,7 @@ class UserProfile(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) supervisor_email = models.EmailField(blank=True) supervisor_name = models.CharField(max_length=100, blank=True) + supervisor = models.ForeignKey("Supervisor", on_delete=models.SET_NULL, blank=True, null=True, related_name="trainee") registration_number = models.CharField(max_length=25, blank=True) grade = models.ForeignKey(UserGrades, null=True, blank=True, help_text="User grade", on_delete=models.CASCADE) peninsula_trainee = models.BooleanField(default=False) @@ -675,7 +678,28 @@ class UserProfile(models.Model): def getusername(self): return self.user.username + def __str__(self): + return f"Userprofile {self.user}" + username = property(getusername) + +class Supervisor(models.Model): + """Model to hold individual supervisor details (email and name) + + 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") + 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) + active = models.BooleanField(default=True) + + def __str__(self) -> str: + return f"{self.name} ({self.email})" + + def get_absolute_url(self): + return reverse('generic:supervisor_detail', kwargs={'pk': self.pk}) + @receiver(post_save, sender=User) @@ -685,4 +709,5 @@ def create_user_profile(sender, instance, created, **kwargs): @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs): - instance.userprofile.save() \ No newline at end of file + instance.userprofile.save() + diff --git a/generic/templates/generic/base.html b/generic/templates/generic/base.html index 3b934d00..5fb8a198 100755 --- a/generic/templates/generic/base.html +++ b/generic/templates/generic/base.html @@ -22,6 +22,7 @@ Cid Groups / User Groups / Manage Users / + Manage Supervisors {% endif %} {% endblock %} diff --git a/generic/templates/generic/exam_scores_base.html b/generic/templates/generic/exam_scores_base.html index 3113d4d9..12a128f0 100644 --- a/generic/templates/generic/exam_scores_base.html +++ b/generic/templates/generic/exam_scores_base.html @@ -91,13 +91,13 @@ Email results User results emailed: {{exam.exam_results_emailed|default:"Never"}}
- @@ -30,5 +30,18 @@ Bulk create users + Create single user -{% endblock %} \ No newline at end of file +{% endblock %} + + +{% block css %} + + + +{% endblock css %} + \ No newline at end of file