diff --git a/training/migrations/0010_trainingcategory_training_level_help.py b/training/migrations/0010_trainingcategory_training_level_help.py new file mode 100644 index 00000000..35c0d9db --- /dev/null +++ b/training/migrations/0010_trainingcategory_training_level_help.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.17 on 2026-10-03 15:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('training', '0009_trainingitem_technician_can_train'), + ] + + 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. If not set, any technician may.', null=True, on_delete=django.db.models.deletion.CASCADE, to='training.traininglevel'), + ), + ] diff --git a/training/models.py b/training/models.py index 7e8224d7..18a42fa1 100644 --- a/training/models.py +++ b/training/models.py @@ -57,16 +57,16 @@ class Trainee(Profile, RevisionMixin): return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists() def can_deliver_training(self, item, depth): - """Technicians may deliver training up to Training Complete on flagged items in their own department, - provided they have been passed out on the item themselves.""" + """Technicians may deliver training up to Training Complete on flagged items that they have been passed out on. + 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: return False training_level = item.category.training_level - if training_level is None or training_level.department is None: - return False - return self.confirmed_levels.filter( - level__level=TrainingLevel.TECHNICIAN, level__department=training_level.department - ).exists() and self.is_user_qualified_in(item, TrainingItemQualification.PASSED_OUT) + if training_level is None: + is_permitted = self.is_technician + else: + is_permitted = self.confirmed_levels.filter(level=training_level).exists() + return is_permitted and self.is_user_qualified_in(item, TrainingItemQualification.PASSED_OUT) def get_records_of_depth(self, depth): return self.qualifications_obtained.filter(depth=depth).select_related("item", "trainee", "supervisor") @@ -94,7 +94,8 @@ class TrainingCategory(models.Model): "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", + 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.", ) def __str__(self): @@ -226,14 +227,7 @@ class TrainingItemQualification(models.Model, RevisionMixin): errdict = {} # Validate supervisor can train in this item if hasattr(self, "supervisor"): # This will be false if form validation fails - if self.supervisor.can_deliver_training(self.item, self.depth): - pass - elif self.item.category.training_level: - if not self.supervisor.level_qualifications.filter(level=self.item.category.training_level): - errdict["supervisor"] = ( - "Selected supervising person is missing requisite training level to train in this department" - ) - elif not self.supervisor.is_supervisor: + if not self.supervisor.can_deliver_training(self.item, self.depth) and not self.supervisor.is_supervisor: errdict["supervisor"] = "Selected supervisor must actually *be* a supervisor..." # Item requirements only apply to being passed out if self.depth == TrainingItemQualification.PASSED_OUT and not self.item.user_has_requirements(self.trainee): diff --git a/training/tests/test_unit.py b/training/tests/test_unit.py index c7d379a8..6a4434f0 100644 --- a/training/tests/test_unit.py +++ b/training/tests/test_unit.py @@ -146,6 +146,25 @@ def test_technician_cannot_train_without_passout(trainee, supervisor): ) +def test_any_technician_can_train_if_category_has_no_level(trainee, supervisor): + item = _technician_setup(trainee, supervisor, department=models.TrainingLevel.LIGHTING) + item.category.training_level = None + item.category.save() + assert models.Trainee.objects.get(pk=trainee.pk).can_deliver_training( + item, models.TrainingItemQualification.COMPLETE + ) + + +def test_non_technician_cannot_train_if_category_has_no_level(trainee, supervisor): + item = _technician_setup(trainee, supervisor) + item.category.training_level = None + item.category.save() + models.TrainingLevelQualification.objects.all().delete() + assert not models.Trainee.objects.get(pk=trainee.pk).can_deliver_training( + item, models.TrainingItemQualification.COMPLETE + ) + + def test_technician_cannot_train_other_department(trainee, supervisor): item = _technician_setup(trainee, supervisor, department=models.TrainingLevel.LIGHTING) other = models.TrainingLevel.objects.create(