mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-10 14:52:53 +00:00
Fix training items for Haulage Supervisors
This commit is contained in:
@@ -51,7 +51,7 @@ class Profile(AbstractUser):
|
|||||||
def can_log_training(self):
|
def can_log_training(self):
|
||||||
from training.models import Trainee
|
from training.models import Trainee
|
||||||
|
|
||||||
return self.is_supervisor or Trainee.objects.get(pk=self.pk).is_technician
|
return self.is_supervisor or Trainee.objects.get(pk=self.pk).can_log_training
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
|
|||||||
@@ -80,12 +80,12 @@ class SessionLogForm(forms.Form):
|
|||||||
if supervisor.pk != self.user.pk:
|
if supervisor.pk != self.user.pk:
|
||||||
self.add_error("supervisor", "You may only log sessions that you delivered yourself")
|
self.add_error("supervisor", "You may only log sessions that you delivered yourself")
|
||||||
else:
|
else:
|
||||||
if cleaned_data.get("items_2"):
|
for depth, _ in models.TrainingItemQualification.CHOICES:
|
||||||
self.add_error("items_2", "Technicians may not pass people out")
|
|
||||||
for depth in (models.TrainingItemQualification.STARTED, models.TrainingItemQualification.COMPLETE):
|
|
||||||
for item in cleaned_data.get(f"items_{depth}", []):
|
for item in cleaned_data.get(f"items_{depth}", []):
|
||||||
if not supervisor.can_deliver_training(item, depth):
|
if not supervisor.can_deliver_training(item, depth):
|
||||||
self.add_error(f"items_{depth}", f"You are not permitted to deliver training in {item}")
|
self.add_error(
|
||||||
|
f"items_{depth}", f"You are not permitted to deliver this training in {item}"
|
||||||
|
)
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def clean_date(self):
|
def clean_date(self):
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.2.17 on 2026-10-03 16:37
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('training', '0010_trainingcategory_training_level_help'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='trainingcategory',
|
||||||
|
name='training_level',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='If this is set, technicians must hold this level (e.g. Sound Technician) to deliver training in technician-trainable items in this category, and holders of a Supervisor level (e.g. Haulage Supervisor) may pass people out in it. If not set, any technician may deliver training in technician-trainable items.', null=True, on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -56,12 +56,34 @@ class Trainee(Profile, RevisionMixin):
|
|||||||
def is_driver(self):
|
def is_driver(self):
|
||||||
return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists()
|
return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_log_training(self):
|
||||||
|
"""Whether this person may use the training logging pages at all"""
|
||||||
|
category_levels = TrainingCategory.objects.exclude(training_level=None).values("training_level")
|
||||||
|
return (
|
||||||
|
self.is_supervisor or self.is_technician or self.confirmed_levels.filter(level__in=category_levels).exists()
|
||||||
|
)
|
||||||
|
|
||||||
def can_deliver_training(self, item, depth):
|
def can_deliver_training(self, item, depth):
|
||||||
"""Technicians may deliver training up to Training Complete on flagged items that they have been passed out on.
|
"""Whether this person may record training of the given depth in the item (i.e. be named as the supervisor)
|
||||||
If the item's category specifies a training level, they must also hold that level, otherwise any technician will do."""
|
|
||||||
if depth == TrainingItemQualification.PASSED_OUT or not item.technician_can_train:
|
- Supervisors may deliver anything.
|
||||||
return False
|
- Passing out requires holding a Supervisor level set on the item's category (e.g. Haulage Supervisor)
|
||||||
|
and being passed out in the item oneself.
|
||||||
|
- Started/Complete require the item to be technician trainable and the person to be passed out in it
|
||||||
|
themselves, and to hold the category's level, or to be a technician if the category has none."""
|
||||||
|
if self.is_supervisor:
|
||||||
|
return True
|
||||||
training_level = item.category.training_level
|
training_level = item.category.training_level
|
||||||
|
if depth == TrainingItemQualification.PASSED_OUT:
|
||||||
|
return (
|
||||||
|
training_level is not None
|
||||||
|
and training_level.level == TrainingLevel.SUPERVISOR
|
||||||
|
and self.confirmed_levels.filter(level=training_level).exists()
|
||||||
|
and self.is_user_qualified_in(item, TrainingItemQualification.PASSED_OUT)
|
||||||
|
)
|
||||||
|
if not item.technician_can_train:
|
||||||
|
return False
|
||||||
if training_level is None:
|
if training_level is None:
|
||||||
is_permitted = self.is_technician
|
is_permitted = self.is_technician
|
||||||
else:
|
else:
|
||||||
@@ -95,7 +117,7 @@ class TrainingCategory(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="If this is set, technicians must hold this level (e.g. Sound Technician) to deliver training in technician-trainable items in this category. If not set, any technician may.",
|
help_text="If this is set, technicians must hold this level (e.g. Sound Technician) to deliver training in technician-trainable items in this category, and holders of a Supervisor level (e.g. Haulage Supervisor) may pass people out in it. If not set, any technician may deliver training in technician-trainable items.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -227,7 +249,7 @@ class TrainingItemQualification(models.Model, RevisionMixin):
|
|||||||
errdict = {}
|
errdict = {}
|
||||||
# Validate supervisor can train in this item
|
# Validate supervisor can train in this item
|
||||||
if hasattr(self, "supervisor"): # This will be false if form validation fails
|
if hasattr(self, "supervisor"): # This will be false if form validation fails
|
||||||
if not self.supervisor.can_deliver_training(self.item, self.depth) and not self.supervisor.is_supervisor:
|
if not self.supervisor.can_deliver_training(self.item, self.depth):
|
||||||
errdict["supervisor"] = "Selected supervisor must actually *be* a supervisor..."
|
errdict["supervisor"] = "Selected supervisor must actually *be* a supervisor..."
|
||||||
# Item requirements only apply to being passed out
|
# Item requirements only apply to being passed out
|
||||||
if self.depth == TrainingItemQualification.PASSED_OUT and not self.item.user_has_requirements(self.trainee):
|
if self.depth == TrainingItemQualification.PASSED_OUT and not self.item.user_has_requirements(self.trainee):
|
||||||
|
|||||||
@@ -61,11 +61,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="collapse mt-3" id="deliverable_items">
|
<div class="collapse mt-3" id="deliverable_items">
|
||||||
<div class="card card-body">
|
<div class="card card-body">
|
||||||
<p class="text-muted">You may deliver up to Training Complete in these items, as you are a technician who is passed out in them. Passing people out is for supervisors only.</p>
|
<p class="text-muted">You may deliver training in these items. Passing people out is only possible where marked.</p>
|
||||||
{% for category, items in deliverable_items %}
|
{% for category, items in deliverable_items %}
|
||||||
<h5>{{ category }}</h5>
|
<h5>{{ category }}</h5>
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in items %}<li>{{ item.display_name }}</li>{% endfor %}
|
{% for item in items %}<li>{{ item.display_name }}{% if item.can_pass_out %} <span class="text-muted">(including passing out)</span>{% endif %}</li>{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<p class="mb-0">You can't currently deliver training in any items. An item must be marked as technician-trainable, you must be passed out in it, and you must hold any technician level its category requires.</p>
|
<p class="mb-0">You can't currently deliver training in any items. An item must be marked as technician-trainable, you must be passed out in it, and you must hold any technician level its category requires.</p>
|
||||||
|
|||||||
@@ -215,3 +215,57 @@ def test_plain_trainee_cannot_log_session(client, trainee):
|
|||||||
def test_supervisor_does_not_see_deliverable_items(admin_client):
|
def test_supervisor_does_not_see_deliverable_items(admin_client):
|
||||||
response = admin_client.get(reverse("session_log"))
|
response = admin_client.get(reverse("session_log"))
|
||||||
assertNotContains(response, "What training items can I deliver training in?")
|
assertNotContains(response, "What training items can I deliver training in?")
|
||||||
|
|
||||||
|
|
||||||
|
def _haulage_setup(trainee, supervisor):
|
||||||
|
haul_level = models.TrainingLevel.objects.create(
|
||||||
|
level=models.TrainingLevel.SUPERVISOR, department=models.TrainingLevel.HAULAGE, description="x"
|
||||||
|
)
|
||||||
|
models.TrainingLevelQualification.objects.create(
|
||||||
|
trainee=models.Trainee.objects.get(pk=trainee.pk), level=haul_level, confirmed_on=timezone.now()
|
||||||
|
)
|
||||||
|
category = models.TrainingCategory.objects.create(reference_number=4, name="Haulage", training_level=haul_level)
|
||||||
|
return models.TrainingItem.objects.create(category=category, reference_number=1, name="Tail lifts")
|
||||||
|
|
||||||
|
|
||||||
|
def test_haulage_supervisor_can_pass_out_without_supervisor_flag(client, trainee, supervisor, admin_user):
|
||||||
|
item = _haulage_setup(trainee, supervisor)
|
||||||
|
tech = models.Trainee.objects.get(pk=trainee.pk)
|
||||||
|
assert not tech.is_supervisor
|
||||||
|
assert tech.can_log_training
|
||||||
|
# Must be passed out in the item oneself
|
||||||
|
assert not tech.can_deliver_training(item, models.TrainingItemQualification.PASSED_OUT)
|
||||||
|
models.TrainingItemQualification.objects.create(
|
||||||
|
item=item,
|
||||||
|
depth=models.TrainingItemQualification.PASSED_OUT,
|
||||||
|
trainee=tech,
|
||||||
|
supervisor=models.Trainee.objects.get(pk=supervisor.pk),
|
||||||
|
date=datetime.date.today(),
|
||||||
|
)
|
||||||
|
assert tech.can_deliver_training(item, models.TrainingItemQualification.PASSED_OUT)
|
||||||
|
client.force_login(trainee)
|
||||||
|
url = reverse("session_log")
|
||||||
|
data = {
|
||||||
|
"trainees": [admin_user.pk],
|
||||||
|
"items_2": [item.pk],
|
||||||
|
"supervisor": trainee.pk,
|
||||||
|
"date": datetime.date.today().strftime("%Y-%m-%d"),
|
||||||
|
}
|
||||||
|
assert client.post(url, data).status_code == 302
|
||||||
|
assert models.TrainingItemQualification.objects.filter(
|
||||||
|
trainee=admin_user.pk, item=item, depth=models.TrainingItemQualification.PASSED_OUT
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_technician_level_does_not_allow_pass_out(trainee, supervisor):
|
||||||
|
item = _technician_setup(trainee, supervisor)
|
||||||
|
assert not models.Trainee.objects.get(pk=trainee.pk).can_deliver_training(
|
||||||
|
item, models.TrainingItemQualification.PASSED_OUT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flagged_supervisor_can_deliver_anything(trainee, supervisor):
|
||||||
|
item = _technician_setup(trainee, supervisor)
|
||||||
|
sup = models.Trainee.objects.get(pk=supervisor.pk)
|
||||||
|
for depth, _ in models.TrainingItemQualification.CHOICES:
|
||||||
|
assert sup.can_deliver_training(item, depth)
|
||||||
|
|||||||
@@ -299,12 +299,15 @@ class SessionLog(generic.FormView):
|
|||||||
def get_deliverable_items(self):
|
def get_deliverable_items(self):
|
||||||
"""Active items the current user may deliver training in, grouped by category, using the same test as form validation"""
|
"""Active items the current user may deliver training in, grouped by category, using the same test as form validation"""
|
||||||
trainee = models.Trainee.objects.get(pk=self.request.user.pk)
|
trainee = models.Trainee.objects.get(pk=self.request.user.pk)
|
||||||
items = models.TrainingItem.objects.filter(active=True, technician_can_train=True).select_related(
|
items = (
|
||||||
"category", "category__training_level"
|
models.TrainingItem.objects.filter(active=True)
|
||||||
|
.filter(Q(technician_can_train=True) | Q(category__training_level__level=models.TrainingLevel.SUPERVISOR))
|
||||||
|
.select_related("category", "category__training_level")
|
||||||
)
|
)
|
||||||
grouped = {}
|
grouped = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
if trainee.can_deliver_training(item, models.TrainingItemQualification.COMPLETE):
|
item.can_pass_out = trainee.can_deliver_training(item, models.TrainingItemQualification.PASSED_OUT)
|
||||||
|
if item.can_pass_out or trainee.can_deliver_training(item, models.TrainingItemQualification.COMPLETE):
|
||||||
grouped.setdefault(item.category, []).append(item)
|
grouped.setdefault(item.category, []).append(item)
|
||||||
return list(grouped.items())
|
return list(grouped.items())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user