Compare commits

...

7 Commits

13 changed files with 131 additions and 109 deletions

View File

@@ -19,7 +19,7 @@ class QualificationForm(forms.ModelForm):
pk = kwargs.pop('pk', None)
super(QualificationForm, self).__init__(*args, **kwargs)
self.fields['trainee'].initial = Profile.objects.get(pk=pk)
self.fields['date'].initial = date.today()
self.fields['date'].widget.format = '%Y-%m-%d'
def clean_date(self):
date = self.cleaned_data['date']

View File

@@ -146,7 +146,7 @@ class Command(BaseCommand):
supervisor=supervisor
)
notes = child.find('{}_Notes'.format(depth))
if notes:
if notes is not None:
obj.notes = notes.text
obj.save()
if created:

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.1.13 on 2022-01-02 20:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('training', '0011_auto_20220102_1106'),
]
operations = [
migrations.AlterField(
model_name='traininglevel',
name='department',
field=models.IntegerField(blank=True, choices=[(0, 'Sound'), (1, 'Lighting'), (2, 'Power'), (3, 'Rigging'), (4, 'Haulage')], null=True),
),
migrations.AlterField(
model_name='traininglevel',
name='description',
field=models.TextField(blank=True),
),
]

View File

@@ -16,9 +16,17 @@ class Trainee(Profile, RevisionMixin):
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 is_technician(self):
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
.filter(level__level=TrainingLevel.TECHNICIAN) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@property
def is_supervisor(self):
return self.level_qualifications.all().exclude(confirmed_on=None).select_related('level') \
return self.level_qualifications.exclude(confirmed_on=None).select_related('level') \
.filter(level__level__gte=TrainingLevel.SUPERVISOR) \
.exclude(level__department=TrainingLevel.HAULAGE) \
.exclude(level__department__isnull=True).exists()
@@ -66,7 +74,7 @@ class TrainingItem(models.Model):
@staticmethod
def user_has_qualification(item, user, depth):
return user.qualifications_obtained.values('item', 'depth').filter(item=item, depth__gte=depth).exists()
return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists()
class Meta:
unique_together = ["reference_number", "active", "category"]
@@ -93,7 +101,7 @@ class TrainingItemQualification(models.Model):
# TODO Maximum depth - some things stop at Complete and you can't be passed out in them
def __str__(self):
return "{} in {} on {}".format(self.depth, self.item, self.date)
return "{} in {} on {}".format(self.get_depth_display(), self.item, self.date.strftime("%b %d %Y"))
@property
def activity_feed_string(self):
@@ -119,7 +127,7 @@ class TrainingItemQualification(models.Model):
# Levels
@reversion.register(follow=["requirements"])
class TrainingLevel(models.Model, RevisionMixin):
description = models.CharField(max_length=120, blank=True)
description = models.TextField(blank=True)
TA = 0
TECHNICIAN = 1
SUPERVISOR = 2
@@ -140,7 +148,7 @@ class TrainingLevel(models.Model, RevisionMixin):
(RIGGING, 'Rigging'),
(HAULAGE, 'Haulage'),
)
department = models.IntegerField(choices=DEPARTMENTS, null=True) # N.B. Technical Assistant does not have a department
department = models.IntegerField(choices=DEPARTMENTS, null=True, blank=True) # N.B. Technical Assistant does not have a department
level = models.IntegerField(choices=CHOICES)
prerequisite_levels = models.ManyToManyField('self', related_name='prerequisites', symmetrical=False, blank=True)
icon = models.CharField(null=True, blank=True, max_length=20)
@@ -211,6 +219,14 @@ class TrainingLevel(models.Model, RevisionMixin):
def get_absolute_url(self):
return reverse('level_detail', kwargs={'pk': self.pk})
@property
def get_icon(self):
if self.icon is not None:
icon = "<span class='fas fa-{}'></span>".format(self.icon)
else:
icon = "".join([w[0] for w in str(self).split()])
return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.department_colour, str(self), icon))
@reversion.register
class TrainingLevelRequirement(models.Model, RevisionMixin):
@@ -238,11 +254,7 @@ class TrainingLevelQualification(models.Model, RevisionMixin):
@property
def get_icon(self):
if self.level.icon is not None:
icon = "<span class='fas fa-{}'></span>".format(self.level.icon)
else:
icon = "".join([w[0] for w in str(self.level).split()])
return mark_safe("<span class='badge badge-{} badge-pill' data-toggle='tooltip' title='{}'>{}</span>".format(self.level.department_colour, self.level, icon))
return self.level.get_icon
def __str__(self):
if self.level.is_common_competencies:

View File

@@ -31,6 +31,9 @@
<div class="form-group form-row">
<label for="item_id" class="col-sm-2 col-form-label">Item</label>
<select name="item" id="item_id" class="form-control selectpicker custom-select col-sm-4" data-live-search="true" data-sourceurl="{% url 'api_secure' model='training_item' %}?fields=reference_number,name&filters=active" required>
{% if object.item %}
<option value="{{object.item.pk}}" selected>{{object.item}}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row">
@@ -40,20 +43,24 @@
<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" required>
{% if object.supervisor %}
<option value="{{object.supervisor.pk}}" selected>{{object.supervisor}}</option>
{% endif %}
</select>
</div>
<div class="form-group form-row">
<label for="date" class="col-sm-2 col-form-label">Training Date</label>
<div class="col-sm-8">
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=form.date.initial %}
{% with training_date=object.date|date:"Y-m-d" %}
{% render_field form.date|add_class:'form-control'|attr:'type="date"' value=training_date %}
{% endwith %}
</div>
<button class="btn btn-info col-sm-2" onclick="var date = new Date(); $('#id_date').val([date.getFullYear(), ('0' + (date.getMonth()+1)).slice(-2), ('0' + date.getDate()).slice(-2)].join('-'))" tabindex="-1" type="button">Today</button>
</div>
<div class="form-group form-row">
<label for="item_description" class="col-sm-2 col-form-label">Notes</label>
<div class="col-sm-10">
<textarea type="text" placeholder="Notes" class="form-control"
id="notes" rows="3"></textarea>
<label for="id_notes" class="col-sm-2 col-form-label">Notes</label>
<div class="col-sm-8">
{% render_field form.notes|add_class:'form-control' rows=3 %}
</div>
</div>
{% if not request.is_ajax %}

View File

@@ -16,7 +16,7 @@
{% if item.active %}
<li class="list-group-item">{{ item }}</li>
{% elif request.user.is_superuser %}
<li class="list-group-item text-warning">{{ item }} (inactive)</li>
<li class="list-group-item text-warning">{{ item }}</li>
{% endif %}
{% endfor %}
</div>

View File

@@ -62,16 +62,25 @@
<table class="table card-body">
<thead>
<tr>
<th scope="col" class="table-warning">Training Started</th>
<th scope="col" class="table-success">Training Complete</th>
<th scope="col" class="table-info">Passed Out</th>
<th scope="col" class="table-warning" style="width: 33%">Training Started</th>
<th scope="col" class="table-success" style="width: 33%">Training Complete</th>
<th scope="col" class="table-info" style="width: 33%">Passed Out</th>
</tr>
</thead>
<tbody>
<tbody class="table-body">
{% for level in object.prerequisite_levels.all %}
<tr data-toggle="collapse" data-target="#{{level.pk}}" style="cursor: pointer;"><th colspan="3" class="text-center font-italic" data-toggle="collapse" data-target="#level_{{level.pk}}">{{level}} (prerequisite)</th></tr>
<tr id="level_{{level.pk}}" class="collapse">
<td><ul class="list-unstyled">{% for req in level.started_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 0 %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in level.complete_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 1 %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in level.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 2 %}</li>{% endfor %}</ul></td>
</tr>
{% endfor %}
<tr><th colspan="3" class="text-center">{{object}}</th></tr>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 0 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{% endif %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 1 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{% endif %}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 2 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 0 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 1 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} {% user_has_qualification request.user req.item 2 %} {% if request.user.is_supervisor or perms.training.change_traininglevel %}<a type="button" class="btn btn-link tn-sm p-0 align-baseline"" href="{% url 'remove_requirement' pk=req.pk %}" title="Delete requirement"><span class="fas fa-trash-alt text-danger"></span></a>{%endif%}</li>{% endfor %}</ul></td>
</tr>
</tbody>
</table>
@@ -93,7 +102,7 @@
</ul>
</div>
</div>
<div class="card mb-3 d-none d-md-block">
<div class="card mb-3 mt-2">
<h4 class="card-header">Users with this level</h4>
<div class="card-body">
{% for user in users_with %}
@@ -110,7 +119,7 @@
<tbody>
{% endif %}
<tr {% if not level_qualification.confirmed_on %}style="border-style: dashed; opacity: 80%"{%endif%}>
<td><img src="{{user.profile_picture}}" style="width: 50px" class="img-thumbnail"/> {{user}}</td>
<td><a href="{{user.get_absolute_url}}"><img src="{{user.profile_picture}}" style="width: 50px" class="img-thumbnail"/> {{user}}</a></td>
<td>{% if level_qualification.confirmed_on %}<p class="card-text"><small>Qualified on {{ level_qualification.confirmed_on }}</small></p>{%else%}Unconfirmed{%endif%}</td>
<td><a href="{% url 'profile_detail' user.pk %}" class="btn btn-primary btn-sm"><span class="fas fa-user"></span> View Profile</a></div></td>
</tr>
@@ -119,8 +128,8 @@
</table>
{% endif %}
{% empty %}
Nobody here but us chickens... <span class="fas fa-egg text-warning"></span>
{% endfor %}
Nobody here but us chickens... <span class="fas fa-egg text-warning"></span>
{% endfor %}
</div>
</div>
<div class="row">

View File

@@ -3,60 +3,6 @@
{% load markdown_tags %}
{% load get_supervisor from tags %}
{% block css %}
<style>
.tree ul {
margin-left: 20px;
}
.tree li {
list-style-type: none;
margin:10px;
position: relative;
}
.tree li::before {
content: "";
position: absolute;
top:-7px;
left:-20px;
border-left: 1px solid #ccc;
border-bottom:1px solid #ccc;
border-radius:0 0 0 0px;
width:20px;
height:15px;
}
.tree li::after {
position:absolute;
content:"";
top:8px;
left:-20px;
border-left: 1px solid #ccc;
border-top:1px solid #ccc;
border-radius:0px 0 0 0;
width:20px;
height:100%;
}
.tree li:last-child::after {
display:none;
}
.tree li:last-child:before{
border-radius: 0 0 0 5px;
}
ul.tree>li:first-child::before {
display:none;
}
ul.tree>li:first-child::after {
border-radius:5px 0 0 0;
}
</style>
{% endblock %}
{% block content %}
<div class="alert alert-info" role="alert">
<p>Please Note:</p>
@@ -67,19 +13,10 @@ ul.tree>li:first-child::after {
</ul>
<sup>Correct as of 3rd September 2021, check the Training Policy.</sup>
</div>
<ul class="tree">
<li><div class="card"><div class="card-header">{{ta}}</div><div class="card-body">{{ta.description|markdown}}</div></div>
<ul>
{% for level in tech %}
<li><div class="card"><div class="card-header"><a href="{{level.get_absolute_url}}">{{level}}</a></div><div class="card-body">{{level.description|markdown}}</div></div>
<ul>
{% with level|get_supervisor as super %}
<li><div class="card"><div class="card-header"><a href="{{super.get_absolute_url}}">{{super}}</a></div><div class="card-body">{{super.description|markdown}}</div></div></li>
{% endwith %}
</ul>
</li>
{% endfor %}
</ul>
</li>
</ul>
{% for level in object_list %}
<div class="card mb-2">
<div class="card-header">{{level}}</div>
<div class="card-body">{{level.description|markdown}}</div>
</div>
{% endfor %}
{% endblock %}

View File

@@ -7,7 +7,8 @@
{% load colour_from_depth from tags %}
{% block content %}
<div class="row">
<p class="text-muted text-right">Search by supervisor name, item name or item ID</p>{% include 'partials/list_search.html' %}
<div class="row pt-2">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
@@ -18,6 +19,9 @@
<th>Date</th>
<th>Supervisor</th>
<th>Notes</th>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<th></th>
{% endif %}
</tr>
</thead>
<tbody>
@@ -28,6 +32,9 @@
<td>{{ object.date }}</td>
<td><a href="{{ object.supervisor.get_absolute_url}}">{{ object.supervisor }}</a></td>
<td>{{ object.notes }}</td>
{% if request.user.is_supervisor or perms.training.change_trainingitemqualification %}
<td>{% button 'edit' 'edit_qualification' object.pk %}</td>
{% endif %}
</tr>
{% empty %}
<tr class="table-warning">

View File

@@ -35,9 +35,9 @@
{% for object in object_list %}
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name"><a href="{% url 'trainee_detail' object.pk %}">{{ object.name }} {% if request.user.pk == object.pk %}<span class="fas fa-user text-success"></span>{%endif%}</a></th>
<td {% if object.is_driver %}class="table-success"{%endif%}>{{ object.is_driver|yesno|title }}</td>
<td>{{ object.is_driver|yesno|title }}</td>
<td>{% for level in object|get_levels_of_depth:1 %}{% if forloop.first %}Yes {%endif%}{{ level.get_icon }}{%empty%}No{%endfor%}</td>
<td {% if object.is_supervisor %}class="table-success"{%endif%}>{% for level in object|get_levels_of_depth:2 %}{% if forloop.first %}Yes {%endif%}{{ level.get_icon }}{%empty%}No{%endfor%}</td>
<td>{% for level in object|get_levels_of_depth:2 %}{% if forloop.first %}Yes {%endif%}{{ level.get_icon }}{%empty%}No{%endfor%}</td>
<td>{{ object.num_qualifications }} {% if forloop.first %} <span class="fas fa-crown text-warning"></span>{% endif %}</td>
<td style="white-space: nowrap">
<a class="btn btn-info" href="{% url 'trainee_detail' pk=object.pk %}"><span class="fas fa-eye"></span> View Training Record</a>

View File

@@ -11,10 +11,10 @@ register = template.Library()
@register.simple_tag
def user_has_qualification(user, item, depth):
if models.TrainingItem.user_has_qualification(item, user, depth) is not None:
return mark_safe("<span class='fas fa-check text-success'></span>")
if models.TrainingItem.user_has_qualification(item, user, depth):
return mark_safe("<span class='fas fa-check text-success' title='You have this requirement'></span>")
else:
return mark_safe("<span class='fas fa-hourglass-start text-warning'></span>")
return mark_safe("<span class='fas fa-hourglass-start text-warning' title='You do not yet have this requirement'></span>")
@register.simple_tag

View File

@@ -15,6 +15,8 @@ urlpatterns = [
path('trainee/<int:pk>/history', permission_required_with_403('RIGS.view_profile')(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think)
path('trainee/<int:pk>/add_qualification/', login_required(views.AddQualification.as_view()),
name='add_qualification'),
path('trainee/<int:pk>/edit_qualification/', permission_required_with_403('training.change_trainingitemqualification')(views.EditQualification.as_view()),
name='edit_qualification'),
path('session/', login_required(views.SessionLog.as_view()), name='session_log'),
path('levels/', login_required(views.LevelList.as_view()), name='level_list'),
path('level/<int:pk>/', login_required(views.LevelDetail.as_view()), name='level_detail'),

View File

@@ -47,7 +47,18 @@ class TraineeItemDetail(generic.ListView):
template_name = 'trainee_item_list.html'
def get_queryset(self):
return models.Trainee.objects.get(pk=self.kwargs['pk']).qualifications_obtained.all().order_by('-date').select_related('item', 'trainee', 'supervisor', 'item__category')
q = self.request.GET.get('q', "")
filter = Q(item__name__icontains=q) | Q(supervisor__first_name__icontains=q) | Q(supervisor__last_name__icontains=q)
try:
q = q.split('.')
filter = filter | Q(item__category__reference_number=int(q[0]), item__reference_number=int(q[1]))
except: # noqa
# not an integer
pass
return models.Trainee.objects.get(pk=self.kwargs['pk']).qualifications_obtained.all().filter(filter).order_by('-date').select_related('item', 'trainee', 'supervisor', 'item__category')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -62,7 +73,7 @@ class LevelDetail(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Training Level {} <span class='badge badge-{} badge-pill'><span class='fas fa-{}'></span></span>".format(self.object, self.object.department_colour, self.object.icon)
context["page_title"] = "Training Level {} {}".format(self.object, self.object.get_icon)
context["users_with"] = map(lambda qual: qual.trainee, models.TrainingLevelQualification.objects.filter(level=self.object))
return context
@@ -74,9 +85,6 @@ class LevelList(generic.ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "All Training Levels"
context["ta"] = models.TrainingLevel.objects.get(level=models.TrainingLevel.TA)
context["tech"] = models.TrainingLevel.objects.filter(level=models.TrainingLevel.TECHNICIAN).order_by('department')
context["sup"] = models.TrainingLevel.objects.filter(level=models.TrainingLevel.SUPERVISOR).order_by('department')
return context
@@ -141,6 +149,23 @@ class AddQualification(generic.CreateView, ModalURLMixin):
return kwargs
class EditQualification(generic.UpdateView):
template_name = 'edit_training_record.html'
model = models.TrainingItemQualification
form_class = forms.QualificationForm
def get_context_data(self, **kwargs):
context = super(EditQualification, self).get_context_data(**kwargs)
context["depths"] = models.TrainingItemQualification.CHOICES
context['page_title'] = "Edit Qualification {} for {}".format(self.object, models.Trainee.objects.get(pk=self.kwargs['pk']))
return context
def get_form_kwargs(self):
kwargs = super(EditQualification, self).get_form_kwargs()
kwargs['pk'] = self.kwargs['pk']
return kwargs
class AddLevelRequirement(generic.CreateView, ModalURLMixin):
template_name = "add_level_requirement.html"
model = models.TrainingLevelRequirement