Improvements to user management

This commit is contained in:
Ross
2022-11-14 14:25:24 +00:00
parent 02eb812122
commit 6e6ba178b5
27 changed files with 515 additions and 20 deletions
+4 -1
View File
@@ -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)
+21 -2
View File
@@ -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"]
# 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 = ("",)
+26
View File
@@ -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)),
],
),
]
@@ -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),
),
]
@@ -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),
),
]
@@ -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),
),
]
@@ -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),
),
]
@@ -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'),
),
]
@@ -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'),
),
]
+29 -4
View File
@@ -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()
instance.userprofile.save()
+1
View File
@@ -22,6 +22,7 @@
<a href="{% url 'generic:cid_group_view' %}">Cid Groups</a> /
<a href="{% url 'generic:user_group_view' %}">User Groups</a> /
<a href="{% url 'accounts_list' %}">Manage Users</a> /
<a href="{% url 'generic:supervisor' %}">Manage Supervisors</a>
{% endif %}
{% endblock %}
@@ -91,13 +91,13 @@
<summary>Email results</summary>
User results emailed: {{exam.exam_results_emailed|default:"Never"}}<br/>
<button id="email-results-button"
title="Email results to users and their supervisors"
title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
>Email user results</button>
<button id="email-unsent-results-button" title="Email results to users and their supervisors"
<button id="email-unsent-results-button" title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email_unsent' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
+12
View File
@@ -0,0 +1,12 @@
{% extends 'generic/base.html' %}
{% block navigation %}
{{block.super}}
<br/>
Supervisors: <a href="{% url 'generic:supervisor' %}">View all</a>
<a href="{% url 'generic:supervisor_create' %}">Create new</a>
{% endblock %}
{% block js %}
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
{% extends 'generic/supervisor_base.html' %}
{% block content %}
<h2>Supervisor: {{object.name}}</h2>
{{object.email}}
<br/>User account: {{object.user}}
<br/>Site: {{object.site}}
<br/>Trainee(s): {% for trainee in object.trainee.all %}
<a href="{% url 'account_profile' trainee.user.username %}">{{trainee.user}}
{% endfor %}
<div>
<a href="{% url 'generic:supervisor_edit' pk=object.pk %}"> Edit</a>
<a href="{% url 'generic:supervisor_delete' pk=object.pk %}"> Delete</a>
</div>
{% endblock %}
{% block js %}
<style>
td, th { padding-left: 10px }
</style>
{% endblock %}
@@ -0,0 +1,32 @@
{% extends "generic/supervisor_base.html" %}
<!-- {% load static from static %} -->
{% block css %}
<style>
</style>
{% endblock %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
{{form.media}}
<script type="text/javascript">
</script>
<!-- {{ form.media }} -->
{% endblock %}
{% block content %}
<h2>Edit Supervisor / {{object.cid}}</h2>
Use this form to create / edit a supervisor
{% 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.as_table }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
</form>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
{% extends 'generic/supervisor_base.html' %}
{% block content %}
<h2>Supervisors</h2>
<ul>
{% for supervisor in object_list %}
<li><a href="{% url 'generic:supervisor_detail' pk=supervisor.pk %}">Supervisor: {{supervisor.name}}</a>
<br/>{{supervisor.email}}
</li>
{% endfor %}
</ul>
{% endblock %}
{% block js %}
<style>
td, th { padding-left: 10px }
</style>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
{% extends "generic/base.html" %}
<!-- {% load static from static %} -->
{% 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>Edit User / {{ciduser.cid}}</h2>
Use this form to create a user.
{% 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.as_table }}
</table>
<input type="submit" class="submit-button" value="Submit" name="submit">
</form>
{% endblock %}
+5
View File
@@ -92,6 +92,11 @@ urlpatterns = [
),
path("cids/create", views.manage_cid_users, name="manage_cid_users"),
path("cids/create/email", views.create_cid_email, name="create_cid_email"),
path("supervisor", views.SupervisorList.as_view(), name="supervisor"),
path("supervisor/<int:pk>", views.SupervisorDetail.as_view(), name="supervisor_detail"),
path("supervisor/<int:pk>/edit", views.SupervisorUpdate.as_view(), name="supervisor_edit"),
path("supervisor/create", views.SupervisorCreate.as_view(), name="supervisor_create"),
path("supervisor/<int:pk>/delete", views.SupervisorDelete.as_view(), name="supervisor_delete"),
]
+87 -2
View File
@@ -1,5 +1,6 @@
from collections import defaultdict, Counter
from pathlib import Path
import secrets
import statistics
import threading
from dal import autocomplete
@@ -35,6 +36,8 @@ import urllib
from django.views import View
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django_filters.views import FilterView
from django_filters import FilterSet, OrderingFilter
from django_tables2.views import SingleTableMixin
@@ -49,10 +52,12 @@ from .forms import (
CidUserForm,
ExaminationForm,
CidUserGroupForm,
SupervisorForm,
UserUserForm,
UserUserGroupForm,
)
from .models import CidUser, CidUserGroup, Examination, QuestionNote, UserUserGroup, get_next_cid
from .models import CidUser, CidUserGroup, Examination, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup, get_next_cid
from rapids.models import Rapid as RapidQuestion
from rapids.models import Exam as RapidExam
@@ -2500,4 +2505,84 @@ class ExamDeleteBase(RevisionMixin, DeleteView):
def exam_inactive(request, context):
return render(request, "exam_inactive.html", context)
return render(request, "exam_inactive.html", context)
#class UserUserCreate(CidManagerRequiredMixin, CreateView):
# model = User
# form_class = UserUserForm
# template_name: str = "generic/user_creation_form.html"
#
# success_url = reverse_lazy("accounts_list")
def create_user(request, context=None):
# 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)
# check whether it's valid:
if form.is_valid():
try:
user_dict = {
"username": request.POST["username"],
"first_name": request.POST["first_name"],
"last_name": request.POST["last_name"],
"email": request.POST["username"],
"password": secrets.token_hex(nbytes=16)
}
new_user = User.objects.create_user(**user_dict)
except Exception as error:
errors = f"Unable to create account: {error}"
return render(request, 'generic/user_creation_form.html', {'form': form, "errors": errors})
try:
user_profile = UserProfile.objects.get(user=new_user)
user_profile.peninsula_trainee = True
#if "supervisor_email" in user:
# user_profile.supervisor_email = user["supervisor_email"]
#if "supervisor_name" in user:
# user_profile.supervisor_name = user["supervisor_name"]
if request.POST["grade"]:
grade = UserGrades.objects.get(name=f"ST{ request.POST['grade'] }")
user_profile.grade = grade
user_profile.save()
except Exception as error:
errors = f"Unable to create account profile {error}"
return render(request, 'generic/user_creation_form.html', {'form': form, "errors": errors})
return HttpResponseRedirect(reverse_lazy("accounts_list"))
return reverse_lazy("accounts_list")
# if a GET (or any other method) we'll create a blank form
else:
form = UserUserForm()
return render(request, 'generic/user_creation_form.html', {'form': form})
class SupervisorDetail(CidManagerRequiredMixin, DetailView):
model = Supervisor
class SupervisorDelete(CidManagerRequiredMixin, DeleteView):
model = Supervisor
template_name: str = "confirm_delete.html"
success_url = reverse_lazy("generic:supervisor")
class SupervisorUpdate(CidManagerRequiredMixin, UpdateView):
model = Supervisor
form_class = SupervisorForm
#success_url = reverse_lazy("generic:supervisor_detail", kwargs={'pk': self.pk})
class SupervisorCreate(CidManagerRequiredMixin, CreateView):
model = Supervisor
form_class = SupervisorForm
#success_url = reverse_lazy("generic:supervisor_detail")
class SupervisorList(CidManagerRequiredMixin, ListView):
model = Supervisor