Compare commits

...

5 Commits

Author SHA1 Message Date
3ae507b469 Filter trainees for active approved users
Closes #477
2022-01-25 13:08:33 +00:00
33754eed60 System for allowing certain TrainingCategories to be trained by certain levels, regardless of supervisor status
I.e. the haulage department, ref #482. As generic as I can make it I think.
2022-01-25 13:04:26 +00:00
15ab626593 HOTFIX: Version string broken on paperwork generation
Why the hell didn't the tests catch that?
2022-01-25 12:30:37 +00:00
7bc47b446c Add functionality to filter trainee list by is_supervisor
Closes #479
2022-01-25 10:53:25 +00:00
83b287a418 Refactor merge logic to allow merging of users. Closes #473. 2022-01-25 10:29:46 +00:00
10 changed files with 119 additions and 51 deletions

View File

@@ -8,6 +8,7 @@ from django.db.models import Count
from django.forms import ModelForm
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.db import IntegrityError
from reversion import revisions as reversion
from reversion.admin import VersionAdmin
@@ -21,45 +22,11 @@ admin.site.register(models.EventItem, VersionAdmin)
admin.site.register(models.Invoice, VersionAdmin)
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
approve_user.short_description = "Approve selected users"
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
# Don't know how to add 'is_approved' whilst preserving the default list...
list_filter = ('is_approved', 'is_active', 'is_staff', 'is_superuser', 'groups')
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = [approve_user]
class AssociateAdmin(VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
merge_fields = ['name']
def get_queryset(self, request):
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
@@ -71,17 +38,37 @@ class AssociateAdmin(VersionAdmin):
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
masterObjectPk = request.POST.get('master')
masterObject = queryset.get(pk=masterObjectPk)
master_object_pk = request.POST.get('master')
master_object = queryset.get(pk=master_object_pk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
with transaction.atomic(), reversion.create_revision():
for obj in queryset.exclude(pk=masterObjectPk):
events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
for obj in queryset.exclude(pk=master_object_pk):
# If we're merging profiles, merge their training information
if hasattr(obj, 'event_mic'):
events = obj.event_mic.all()
for event in events:
master_object.event_mic.add(event)
for qual in obj.qualifications_obtained.all():
try:
with transaction.atomic():
master_object.qualifications_obtained.add(qual)
except IntegrityError:
existing_qual = master_object.qualifications_obtained.get(item=qual.item, depth=qual.depth)
existing_qual.notes += qual.notes
existing_qual.save()
for level in obj.level_qualifications.all():
try:
with transaction.atomic():
master_object.level_qualifications.add(level)
except IntegrityError:
continue # Exists, oh well
else:
events = obj.event_set.all()
for event in events:
master_object.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
@@ -107,6 +94,35 @@ class AssociateAdmin(VersionAdmin):
return TemplateResponse(request, 'admin_associate_merge.html', context)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin, AssociateAdmin):
list_display = ('username', 'name', 'is_approved', 'is_staff', 'is_superuser', 'is_supervisor', 'number_of_events')
list_display_links = ['username']
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}),
(_('Important dates'), {
'fields': ('last_login', 'date_joined')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'password1', 'password2'),
}),
)
form = user_forms.ProfileChangeForm
add_form = user_forms.ProfileCreationForm
actions = ['approve_user', 'merge']
merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor']
def approve_user(modeladmin, request, queryset):
queryset.update(is_approved=True)
@admin.register(models.Person)
class PersonAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')

View File

@@ -106,7 +106,7 @@
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
{{info_string}}
</drawCenteredString>
</pageGraphics>
@@ -122,7 +122,7 @@
<drawCenteredString x="302.5" y="38">[Page <pageNumber/> of <getName id="lastPage" default="0" />]</drawCenteredString>
<setFont name="OpenSans" size="7" />
<drawCenteredString x="302.5" y="26">
[Paperwork generated{% if current_user %} by {{current_user.name}} |{% endif %} {% now "d/m/Y H:i" %} | {{object.current_version_id}}]
{{info_string}}
</drawCenteredString>
</pageGraphics>
<frame id="main" x1="50" y1="65" width="495" height="727"/>

View File

@@ -185,11 +185,15 @@ class EventPrint(generic.View):
merger = PdfFileMerger()
user_str = f"by {request.user.name} " if request.user is not None else ""
time = timezone.now().strftime('%d/%m/%Y %H:%I')
context = {
'object': object,
'quote': True,
'current_user': request.user,
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date)
'filename': 'Event_{}_{}_{}.pdf'.format(object.display_id, re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name), object.start_date),
'info_string': f"[Paperwork generated {user_str}on {time} - {object.current_version_id}]",
}
rml = template.render(context)

View File

@@ -23,9 +23,13 @@ class QualificationForm(forms.ModelForm):
def clean_supervisor(self):
supervisor = self.cleaned_data['supervisor']
item = self.cleaned_data['item']
if supervisor.pk == self.cleaned_data['trainee'].pk:
raise forms.ValidationError('One may not supervise oneself...')
if not supervisor.is_supervisor:
if item.category.training_level:
if not supervisor.level_qualifications.filter(level=item.category.training_level):
raise forms.ValidationError('Selected supervising person is missing requisite training level to train in this department')
elif not supervisor.is_supervisor:
raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...')
return supervisor

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.2.11 on 2022-01-25 12:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('training', '0002_alter_traininglevel_options'),
]
operations = [
migrations.AddField(
model_name='trainingcategory',
name='training_level',
field=models.ForeignKey(help_text='If this is set, any user with the selected level may pass out users within this category, regardless of other status', null=True, on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel'),
),
]

View File

@@ -6,31 +6,42 @@ from django.utils.safestring import mark_safe
from versioning.versioning import RevisionMixin
class TraineeManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True, is_approved=True)
@reversion.register(for_concrete_model=False, fields=[])
class Trainee(Profile, RevisionMixin):
class Meta:
proxy = True
objects = TraineeManager()
# FIXME use queryset
def started_levels(self):
return [level for level in TrainingLevel.objects.all() if level.percentage_complete(self) > 0 and level.pk not in self.level_qualifications.values_list('level', flat=True)]
@property
def confirmed_levels(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level')
@property
def is_technician(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
return self.confirmed_levels \
.filter(level__level=TrainingLevel.TECHNICIAN) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@property
def is_driver(self):
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level').filter(level__department=TrainingLevel.HAULAGE).exists()
return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists()
def get_records_of_depth(self, depth):
return self.qualifications_obtained.filter(depth=depth).select_related('item', 'trainee', 'supervisor')
def is_user_qualified_in(self, item, required_depth):
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).first() is not None # this is a somewhat ghetto version of get_or_none
return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).exists()
def get_absolute_url(self):
return reverse('trainee_detail', kwargs={'pk': self.pk})
@@ -47,6 +58,7 @@ class Trainee(Profile, RevisionMixin):
class TrainingCategory(models.Model):
reference_number = models.IntegerField(unique=True)
name = models.CharField(max_length=50)
training_level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE, null=True, help_text="If this is set, any user with the selected level may pass out users within this category, regardless of other status")
def __str__(self):
return f"{self.reference_number}. {self.name}"

View File

@@ -42,7 +42,7 @@
</div>
<div class="form-group form-row">
<label for="supervisor" class="col-sm-2 col-form-label">Supervisor</label>
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials&filters=is_supervisor" required>
<select name="supervisor" id="supervisor_id" class="form-control selectpicker custom-select col-sm-10" data-live-search="true" data-sourceurl="{% url 'api_secure' model='profile' %}?fields=first_name,last_name,initials" required>
{% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %}

View File

@@ -16,7 +16,17 @@
{% endblock %}
{% block content %}
{% include 'partials/list_search.html' %}
<form method="GET" class="ml-auto w-25 d-flex flex-column justify-content-end">
{% csrf_token %}
<div class="input-group">
<input type="search" name="q" placeholder="Search" value="{{ request.GET.q }}"
class="form-control" id="id_search_text"/>
<span class="input-group-append">{% button 'search' id="id_search" %}</span>
</div>
<button type="submit" class="btn btn-primary mt-2 {% if request.GET.is_supervisor %}active{%endif%}" data-toggle="button" aria-pressed="{% if request.GET.is_supervisor %}true{%endif%}" name="is_supervisor" value="{% if request.GET.is_supervisor %}{% else %}True{% endif %}">
Only Supervisors
</button>
</form>
<div class="row pt-2">
<div class="col">
<div class="table-responsive">

View File

@@ -108,6 +108,9 @@ class TraineeList(generic.ListView):
# not an integer
pass
if self.request.GET.get('is_supervisor', ''):
filt = filt & Q(is_supervisor=True)
return self.model.objects.filter(filt).annotate(num_qualifications=Count('qualifications_obtained')).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item')
def get_context_data(self, **kwargs):

View File

@@ -9,12 +9,12 @@ from reversion.models import Version, VersionQuerySet
class RevisionMixin:
@property
def is_first_version(self):
versions = Version.objects.get_for_object(self)
versions = RIGSVersion.objects.get_for_object(self)
return len(versions) == 1
@property
def current_version(self):
version = Version.objects.get_for_object(self).select_related('revision').first()
version = RIGSVersion.objects.get_for_object(self).select_related('revision').first()
return version
@property