Compare commits

...

8 Commits

Author SHA1 Message Date
9590c2066d Validate that only supervisors may be supervisors 2021-08-19 16:19:46 +01:00
8b48b02ca7 Force trainingitemqualifications to be unique 2021-08-19 16:00:31 +01:00
68e7ec2a0d Merge branch 'master' into training 2021-08-19 15:49:16 +01:00
11636809ca Add link to subhire insurance form on event detail 2021-08-19 15:48:56 +01:00
5779ebdf7e Merge branch 'master' into training
# Conflicts:
#	templates/base.html
2021-08-17 21:35:10 +01:00
d7458f6366 Account for null power MICs in event checklist detail 2021-08-16 20:28:30 +01:00
febf9cf3ed curses! 2021-08-05 12:07:23 +01:00
3322a5ddf8 Add badge to nav for number of waiting invoices
Might slightly help us stop leaving them waiting for far too long...
2021-08-05 11:37:10 +01:00
11 changed files with 48 additions and 34 deletions

View File

@@ -179,24 +179,7 @@ class InvoiceWaiting(generic.ListView):
return context
def get_queryset(self):
return self.get_objects()
def get_objects(self):
# TODO find a way to select items
events = self.model.objects.filter(
(
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) & # Has not already been invoiced
Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person',
'organisation',
'venue', 'mic') \
.prefetch_related('items')
return events
return self.model.objects.waiting_invoices()
class InvoiceEvent(generic.View):

View File

@@ -278,6 +278,19 @@ class EventManager(models.Manager):
).count()
return event_count
def waiting_invoices(self):
events = self.filter(
(
models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before
) & models.Q(invoice__isnull=True) & # Has not already been invoiced
models.Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person', 'organisation', 'venue', 'mic') \
.prefetch_related('items')
return events
@reversion.register(follow=['items'])
class Event(models.Model, RevisionMixin):

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %}
{% load static %}
{% load invoices_waiting from filters %}
{% block titleheader %}
<a class="navbar-brand" href="/">RIGS</a>
@@ -45,11 +46,11 @@
{% if perms.RIGS.view_invoice %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownInvoices" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Invoices
Invoices <span class="badge badge-danger badge-pill">{% invoices_waiting %}</span>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownInvoices">
{% if perms.RIGS.add_invoice %}
<a class="dropdown-item" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting</a>
<a class="dropdown-item text-nowrap" href="{% url 'invoice_waiting' %}"><span class="fas fa-briefcase text-danger"></span> Waiting <span class="badge badge-danger badge-pill">{% invoices_waiting %}</span></a>
{% endif %}
<a class="dropdown-item" href="{% url 'invoice_list' %}"><span class="fas fa-pound-sign text-warning"></span> Outstanding</a>
<a class="dropdown-item" href="{% url 'invoice_archive' %}"><span class="fas fa-book"></span> Archive</a>

View File

@@ -32,7 +32,11 @@
</dd>
<dt class="col-6">{{ object|help_text:'power_mic' }}</dt>
<dd class="col-6">
{% if object.power_mic %}
<a href="{% url 'profile_detail' object.power_mic.pk %}">{{ object.power_mic.name }}</a>
{% else %}
None
{% endif %}
</dd>
</dl>
<p>List vehicles and their drivers</p>

View File

@@ -47,5 +47,7 @@
class="fas fa-pound-sign"></span>
<span class="d-none d-sm-inline">Invoice</span></a>
{% endif %}
<a href="https://docs.google.com/forms/d/e/1FAIpQLSf-TBOuJZCTYc2L8DWdAaC3_Werq0ulsUs8-6G85I6pA9WVsg/viewform" class="btn btn-danger"><span class="fas fa-file-invoice-dollar"></span> Subhire Insurance Form</a>
{% endif %}
</div>

View File

@@ -218,3 +218,8 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
elif type == 'submit':
return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style}
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag
def invoices_waiting():
return len(models.Event.objects.waiting_invoices())

View File

@@ -31,7 +31,7 @@
<a class="skip-link" href='#main'>Skip to content</a>
{% include "analytics.html" %}
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark flex-nowrap" role="navigation">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark flex-nowrap text-nowrap" role="navigation">
<a class="navbar-brand" href="{% if request.user.is_authenticated %}https://members.nottinghamtec.co.uk{%else%}https://nottinghamtec.co.uk{%endif%}">
<img src="{% static 'imgs/logo.webp' %}" width="40" height="40" alt="TEC's Logo: Serif 'TEC' vertically next to a blue box with the words 'PA and Lighting', surrounded by graduated rings" id="logo">
</a>

View File

@@ -1,10 +1,10 @@
{% if user.is_authenticated %}
<form id="searchForm" class="form-inline flex-nowrap mx-md-3 px-2 border border-light rounded" role="form" method="GET" action="{% url 'event_archive' %}">
<form id="searchForm" class="form-inline flex-nowrap mx-md-3 px-2 border border-light rounded w-75" role="form" method="GET" action="{% url 'event_archive' %}">
<div class="input-group input-group-sm flex-nowrap">
<div class="input-group-prepend">
<input id="id_search_input" type="search" name="q" class="form-control form-control-sm" placeholder="Search..." value="{{ request.GET.q }}" />
</div>
<select id="search-options" class="custom-select form-control" style="border-top-right-radius: 0px; border-bottom-right-radius: 0px; width: 20ch;">
<select id="search-options" class="custom-select form-control" style="border-top-right-radius: 0px; border-bottom-right-radius: 0px; width: 15ch;">
<option selected data-action="{% url 'event_archive' %}" href="#">Events</option>
<option data-action="{% url 'person_list' %}" href="#">People</option>
<option data-action="{% url 'organisation_list' %}" href="#">Organisations</option>
@@ -17,7 +17,7 @@
</select>
</div>
<button class="btn btn-info form-control form-control-sm btn-sm w-25" style="border-top-left-radius: 0px;border-bottom-left-radius: 0px;"><span class="fas fa-search"></span><span class="sr-only"> Search</span></button>
<a href="{% url 'search_help' %}" class="nav-link modal-href ml-2"><span class="fas fa-question-circle"></span></a>
<a href="{% url 'search_help' %}" class="nav-link modal-href ml-1"><span class="fas fa-question-circle"></span></a>
</form>
{% endif %}

View File

@@ -19,7 +19,7 @@ class QualificationForm(forms.ModelForm):
super(QualificationForm, self).__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].initial = date.today()
def clean_date(self):
date = self.cleaned_data['date']
if date > date.today():
@@ -30,7 +30,9 @@ class QualificationForm(forms.ModelForm):
supervisor = self.cleaned_data['supervisor']
if supervisor.pk == self.cleaned_data['trainee'].pk:
raise forms.ValidationError('One may not supervise oneself...')
return supervisor # TODO also confirm that the supervisor is a Supervisor
if not supervisor.is_supervisor:
raise forms.ValidationError('Selected supervisor must actually *be* a supervisor...')
return supervisor
class RequirementForm(forms.ModelForm):
depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES)

View File

@@ -10,6 +10,7 @@ class Trainee(Profile):
@property
def is_supervisor(self):
# FIXME Efficiency
for level_qualification in self.levels.select_related('level').all():
if confirmed_on is not None and level_qualification.level.level >= TrainingLevel.SUPERVISOR:
return True
@@ -34,7 +35,7 @@ class TrainingCategory(models.Model):
class TrainingItem(models.Model):
reference_number = models.CharField(max_length=3)
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.RESTRICT)
category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.RESTRICT)
name = models.CharField(max_length=50)
def __str__(self):
@@ -57,10 +58,10 @@ class TrainingItemQualification(models.Model):
(PASSED_OUT, 'Passed Out'),
)
item = models.ForeignKey('TrainingItem', on_delete=models.RESTRICT)
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.RESTRICT)
trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.RESTRICT)
depth = models.IntegerField(choices=CHOICES)
date = models.DateField()
# TODO Remember that some training is external. Support for making an organisation the trainer?
# TODO Remember that some training is external. Support for making an organisation the trainer?
supervisor = models.ForeignKey('Trainee', related_name='qualifications_granted', on_delete=models.RESTRICT)
notes = models.TextField(blank=True)
# TODO Maximum depth - some things stop at Complete and you can't be passed out in them
@@ -74,6 +75,9 @@ class TrainingItemQualification(models.Model):
if level.user_has_requirements(self.trainee):
level_qualification = TrainingLevelQualification.objects.create(trainee=self.trainee, level=level)
class Meta:
unique_together = ["trainee", "item", "depth"]
# Levels
class TrainingLevel(models.Model, RevisionMixin):
@@ -126,7 +130,7 @@ class TrainingLevel(models.Model, RevisionMixin):
return 0
def user_has_requirements(self, user):
return all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.select_related().all())
return all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.select_related().all())
def __str__(self):
if self.department is None: # 2TA
@@ -145,7 +149,7 @@ class TrainingLevelRequirement(models.Model):
class TrainingLevelQualification(models.Model):
trainee = models.ForeignKey('Trainee', related_name='levels', on_delete=models.RESTRICT)
trainee = models.ForeignKey('Trainee', related_name='levels', on_delete=models.RESTRICT)
level = models.ForeignKey('TrainingLevel', on_delete=models.RESTRICT)
confirmed_on = models.DateTimeField(null=True)
confirmed_by = models.ForeignKey('Trainee', related_name='confirmer', on_delete=models.RESTRICT, null=True)

View File

@@ -12,7 +12,7 @@
</div>
<div class="row mb-3">
<h2 class="col-12">Training Levels</h2>
<p>Technical Assistant is conferred automatically when the item requirements are met. Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system. Supervisor status is <em>not automatic</em> and until signed off at a general meeting, does not count.<sup>Correct as of 7th July 2021, check the Training Policy.</sup></p>
<div class="alert alert-info" role="alert">Technical Assistant is conferred automatically when the item requirements are met. Technician status is also automatic, but notification of status should be made at the next general meeting, at which point 'approval' should be granted on the system. Supervisor status is <em>not automatic</em> and until signed off at a general meeting, does not count.<sup>Correct as of 7th July 2021, check the Training Policy.</sup></div>
<div class="card-columns">
{% for level in levels %}
<div class="card my-3">
@@ -21,10 +21,10 @@
<p>{{ level.description|truncatewords:30 }}</p>
<div class="progress mb-2">
{% percentage_complete level object as completion %}
<div class="progress-bar progress-bar-striped" role="progressbar" style="width: {{completion}}%" aria-valuenow="{{completion}}" aria-valuemin="0" aria-valuemax="100">{{completion}}% complete</div>
</div>
<button class="btn btn-link p-0" type="button" data-toggle="collapse" data-target=".reqs_{{level.pk}}" aria-expanded="false" aria-controls="reqs_{{level.pk}}">
Requirements <span class="fas fa-caret-right reqs_{{level.pk}} collapse show"></span><span class="fas fa-caret-down collapse reqs_{{level.pk}}"></span>
</button>