Compare commits

...

4 Commits

11 changed files with 138 additions and 36 deletions

View File

@@ -10,7 +10,6 @@ from django.utils.safestring import SafeData, mark_safe
from django.utils.text import normalize_newlines
from RIGS import models
from training import models as tmodels
register = template.Library()

View File

@@ -31,6 +31,7 @@ class Command(BaseCommand):
self.setup_categories()
self.setup_items()
self.setup_levels()
self.setup_supervisor()
print("Done generating training data")
def setup_categories(self):
@@ -66,3 +67,12 @@ class Command(BaseCommand):
models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0])
self.levels.append(technician)
self.levels.append(supervisor)
def setup_supervisor(self):
supervisor = models.Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor",
initials="SV",
email="supervisor@example.com", is_active=True,
is_staff=True)
supervisor.set_password('supervisor')
supervisor.save()
models.TrainingLevelQualification.objects.create(trainee=supervisor, level=self.levels[-1], confirmed_on=timezone.now())

View File

@@ -12,8 +12,9 @@ class Trainee(Profile):
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:
if level_qualification.confirmed_on is not None and level_qualification.level.level >= TrainingLevel.SUPERVISOR:
return True
return False
def get_records_of_depth(self, depth):
return self.qualifications_obtained.filter(depth=depth).select_related('item', 'trainee', 'supervisor')
@@ -71,9 +72,18 @@ class TrainingItemQualification(models.Model):
def save(self, *args, **kwargs):
super().save()
for level in TrainingLevel.objects.all(): # Mm yes efficiency
for level in TrainingLevel.objects.all(): # Mm yes efficiency FIXME
if level.user_has_requirements(self.trainee):
level_qualification = TrainingLevelQualification.objects.create(trainee=self.trainee, level=level)
level_qualification = TrainingLevelQualification.objects.get_or_create(trainee=self.trainee, level=level)
@classmethod
def get_colour_from_depth(obj, depth):
if depth == 0:
return "warning"
elif depth == 1:
return "success"
else:
return "info"
class Meta:
unique_together = ["trainee", "item", "depth"]

View File

@@ -15,30 +15,40 @@
</div>
</div>
<div class="card mb-3">
<h4 class="card-header">Users with this level</h4>
<div class="card-body">
<p>Users with this level...{% lorem %}</p>
<div class="card-columns">
{% for user in users_with %}
<div class="card w-50">
<img src="{{user.profile_picture}}" class="card-img-top" />
<div class="card-body">
<h5 class="card-title">{{user}}</h5>
<p>Qualified on... </p>
</div>
<div class="card-footer text-right pr-1"><a href="{% url 'profile_detail' user.pk %}" class="btn btn-primary btn-sm"><span class="fas fa-user"></span> View Profile</a></div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="card">
<h4 class="card-header">Level Requirements</h4>
<div class="card-body">
<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>
</tr>
</thead>
<tbody>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} <a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a></li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.complete_requirements %}<li>{{ req.item }} <a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a></li>{% endfor %}</ul></td>
<td><ul class="list-unstyled">{% for req in object.passed_out_requirements %}<li>{{ req.item }} <a type="button" class="btn btn-danger btn-sm" href="{% url 'remove_requirement' pk=req.pk %}"><span class="fas fa-times-circle"></span></a></li>{% endfor %}</ul></td>
</tr>
</tbody>
</table>
</div>
<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>
</tr>
</thead>
<tbody>
<tr>
<td><ul class="list-unstyled">{% for req in object.started_requirements %}<li>{{ req.item }} {% 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 }} {% 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 }} {% 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>
</tr>
</tbody>
</table>
</div>
{% include 'partials/last_edited.html' with target="traininglevel_history" %}
{% endblock %}

View File

@@ -3,6 +3,7 @@
{% load user_has_qualification from tags %}
{% load percentage_complete from tags %}
{% load user_level_if_present from tags %}
{% load colour_from_depth from tags %}
{% block content %}
<div class="col-sm-12 text-right">
@@ -12,7 +13,14 @@
</div>
<div class="row mb-3">
<h2 class="col-12">Training Levels</h2>
<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="alert alert-info" role="alert">
<ul>
<li>Technical Assistant is conferred automatically when the item requirements are met.</li>
<li>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.</li>
<li>Supervisor status is <em>not automatic</em> and until signed off at a general meeting, does not count.</li>
</ul>
<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">
@@ -47,20 +55,20 @@
</table>
</div>
</div>
<div class="card-footer">
<div class="card-footer text-right">
{% user_level_if_present object level as level_qualification %}
{% if level_qualification %}
{% if level_qualification.confirmed_by is None %}
{% if request.user.is_supervisor or request.user.is_superuser %}
<a class="btn btn-info" href="{% url 'confirm_level' object.pk level.pk %}">Confirm</a>
{% else %}
<button class="btn btn-warning text-right" disabled>Awaiting Confirmation</button>
<button class="btn btn-warning" disabled>Awaiting Confirmation</button>
{% endif %}
{% else %}
<button class="btn btn-success active">Confirmed <small>by {{ level_qualification.confirmed_by }}</small></button>
{% endif %}
{% else %}
<button class="btn btn-danger text-right" disabled>Incomplete</button>
<button class="btn btn-danger" disabled>Incomplete</button>
{% endif %}
</div>
</div>
@@ -68,16 +76,16 @@
</div>
</div>
<div class="row">
<h2 class="col-12">Training Items</h2><br>
<h3 class="col-12">Key: <span class="badge badge-warning">Training Started</span> <span class="badge badge-success">Training Complete</span> <span class="badge badge-info">Passed Out</span></h3>
<div class="card-columns">
<h2 class="col-10">Training Items</h2><a href="{% url 'trainee_item_detail' object.pk %}" class="btn btn-info col-2"><span class="fas fa-info-circle"></span> View Detailed Record</a><br/>
<div class="alert alert-info" role="alert"><h3 class="col-12">Key: <span class="badge badge-warning">Training Started</span> <span class="badge badge-success">Training Complete</span> <span class="badge badge-info">Passed Out</span></h3></div>
<div class="card-deck">
{% for category in categories %}
<div class="card mb-3">
<h3 class="card-header">{{ category }}</h3>
<div class="list-group list-group-flush">
{% for q in object.qualifications_obtained.all %}
{% if q.item.category == category %}
<li class="list-group-item {% if q.depth == 0 %}list-group-item-warning{%elif q.depth == 1%}list-group-item-success{%else%}list-group-item-info{%endif%}">{{q.item}} ({{q.date}})</li>
<li class="list-group-item list-group-item-{% colour_from_depth q.depth %}">{{q.item}} ({{q.date}})</li>
{% endif %}
{% endfor %}
</div>

View File

@@ -0,0 +1,42 @@
{% extends 'base_training.html' %}
{% load url_replace from filters %}
{% load paginator from filters %}
{% load linkornone from filters %}
{% load button from filters %}
{% load colour_from_depth from tags %}
{% block content %}
<div class="row">
<div class="col">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Training Item</th>
<th>Depth</th>
<th>Date</th>
<th>Supervisor</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name">{{ object.item }}</th>
<td class="table-{% colour_from_depth object.depth %}">{{ object.get_depth_display }}</td>
<td>{{ object.date }}</td>
<td>{{ object.supervisor }}</td>
<td>{{ object.notes }}</td>
</tr>
{% empty %}
<tr class="table-warning">
<td colspan="6" class="text-center">Nothing found</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,15 +15,18 @@
<tr>
<th scope="col">Name<a href="?{% orderby request 'orderBy' 'name' %}"><span class="caret"></span></a></th>
<th>Supervisor?</th>
<th>-</th>
<th></th>
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr id="row_item">
<th scope="row" class="align-middle" id="cell_name">{{ object.name }}</th>
<td>No</td>
<td><a class="btn btn-info" href="{% url 'trainee_detail' pk=object.pk %}"><span class="fas fa-eye"></span> View Training Record</a></td>
<th scope="row" class="align-middle" id="cell_name">{{ object.name }} {% if request.user.pk == object.pk %}<span class="fas fa-user text-success"></span>{%endif%}</th>
<td {% if object.is_supervisor %}class="table-success"{%endif%}>{{ object.is_supervisor|yesno|title }}</td>
<td>
<a class="btn btn-info" href="{% url 'trainee_detail' pk=object.pk %}"><span class="fas fa-eye"></span> View Training Record</a>
<a href="{% url 'trainee_item_detail' pk=object.pk %}" class="btn btn-info"><span class="fas fa-info-circle"></span> View Detailed Record</a>
</td>
</tr>
{% empty %}
<tr class="table-warning">

View File

@@ -23,3 +23,6 @@ def user_level_if_present(user, level):
def percentage_complete(level, user):
return level.percentage_complete(user)
@register.simple_tag
def colour_from_depth(depth):
return models.TrainingItemQualification.get_colour_from_depth(depth)

View File

@@ -20,4 +20,5 @@ urlpatterns = [
path('level/<int:pk>/add_requirement/', views.AddLevelRequirement.as_view(), name='add_requirement'),
path('level/remove_requirement/<int:pk>/', views.RemoveRequirement.as_view(), name='remove_requirement'),
path('trainee/<int:pk>/level/<int:level_pk>/confirm', views.ConfirmLevel.as_view(), name='confirm_level'),
path('trainee/<int:pk>/item_record', views.TraineeItemDetail.as_view(), name='trainee_item_detail'),
]

View File

@@ -27,7 +27,7 @@ class TraineeDetail(views.ProfileDetail):
def get_context_data(self, **kwargs):
context = super(TraineeDetail, self).get_context_data(**kwargs)
context["page_title"] = "{}'s Training Record".format(self.object)
context["page_title"] = "{}'s Training Record".format(self.object.first_name + " " + self.object.last_name)
context["levels"] = models.TrainingLevel.objects.all()
context["categories"] = models.TrainingCategory.objects.all().prefetch_related('items')
choices = models.TrainingItemQualification.CHOICES
@@ -37,6 +37,19 @@ class TraineeDetail(views.ProfileDetail):
return context
class TraineeItemDetail(generic.ListView):
model = models.TrainingItemQualification
template_name = 'trainee_item_list.html'
def get_queryset(self):
return models.Trainee.objects.get(pk=self.kwargs['pk']).qualifications_obtained.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Detailed Training Record for {}".format(models.Trainee.objects.get(pk=self.kwargs['pk']))
return context
class TraineeList(generic.ListView):
model = models.Trainee
template_name = 'trainee_list.html'
@@ -115,6 +128,7 @@ class LevelDetail(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_title"] = "Training Level {}".format(self.object)
context["users_with"] = map(lambda qual: qual.trainee, models.TrainingLevelQualification.objects.filter(level=self.object))
return context

View File

@@ -20,6 +20,7 @@ class Command(BaseCommand):
hs_group = None
def handle(self, *args, **options):
print("Generating sample user data")
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
@@ -32,6 +33,7 @@ class Command(BaseCommand):
self.setup_groups()
self.setup_useful_profiles()
self.setup_generic_profiles()
print("Done generating sample user data")
def setup_groups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')