diff --git a/RIGS/models.py b/RIGS/models.py index 2b564800..f65d562e 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -47,6 +47,12 @@ class Profile(AbstractUser): reversion_hide = True + @property + def can_log_training(self): + from training.models import Trainee + + return self.is_supervisor or Trainee.objects.get(pk=self.pk).is_technician + @classmethod def make_api_key(cls): size = 20 diff --git a/templates/index.html b/templates/index.html index 4c011db5..5d5fff0b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -56,7 +56,7 @@ Trainee List Level List Item List - {% if request.user.is_supervisor %} + {% if request.user.can_log_training %} Log Session {% endif %} diff --git a/training/admin.py b/training/admin.py index 6493031a..9b4774ef 100644 --- a/training/admin.py +++ b/training/admin.py @@ -4,7 +4,6 @@ from reversion.admin import VersionAdmin admin.site.register(models.TrainingCategory, VersionAdmin) -admin.site.register(models.TrainingItem, VersionAdmin) admin.site.register(models.TrainingLevel, VersionAdmin) admin.site.register(models.TrainingLevelQualification, VersionAdmin) admin.site.register(models.TrainingLevelRequirement, VersionAdmin) @@ -13,3 +12,9 @@ admin.site.register(models.TrainingLevelRequirement, VersionAdmin) @admin.register(models.TrainingItemQualification) class TrainingItemQualificationAdmin(VersionAdmin): list_display = ["__str__", "trainee"] + + +@admin.register(models.TrainingItem) +class TrainingItemAdmin(VersionAdmin): + list_display = ["__str__", "category", "active", "technician_can_train"] + list_filter = ["category", "active", "technician_can_train"] diff --git a/training/decorators.py b/training/decorators.py index 13c66394..20fc66ee 100644 --- a/training/decorators.py +++ b/training/decorators.py @@ -3,3 +3,7 @@ from PyRIGS.decorators import user_passes_test_with_403 def is_supervisor(login_url=None, oembed_view=None): return user_passes_test_with_403(lambda u: hasattr(u, "is_supervisor") and u.is_supervisor) + + +def is_supervisor_or_technician(login_url=None, oembed_view=None): + return user_passes_test_with_403(lambda u: u.is_authenticated and u.can_log_training) diff --git a/training/forms.py b/training/forms.py index 771d1c2a..f81d4e1f 100644 --- a/training/forms.py +++ b/training/forms.py @@ -22,9 +22,12 @@ class QualificationForm(forms.ModelForm): supervisor = self.cleaned_data.get("supervisor") if supervisor.pk == self.cleaned_data.get("trainee").pk: raise forms.ValidationError("One may not supervise oneself...") + if self.user is not None and not self.user.is_supervisor and supervisor.pk != self.user.pk: + raise forms.ValidationError("You may only record training that you delivered yourself") return supervisor def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) super().__init__(*args, **kwargs) self.fields["date"].widget.format = "%Y-%m-%d" @@ -63,6 +66,28 @@ class SessionLogForm(forms.Form): related_models = {"supervisor": models.Trainee} + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + if self.user is not None and not self.user.is_supervisor: + self.fields["supervisor"].initial = self.user.pk + + def clean(self): + cleaned_data = super().clean() + supervisor = cleaned_data.get("supervisor") + # Supervisors may log anything (as before); technicians only what they are permitted to deliver + if self.user is not None and not self.user.is_supervisor and supervisor is not None: + if supervisor.pk != self.user.pk: + self.add_error("supervisor", "You may only log sessions that you delivered yourself") + else: + if cleaned_data.get("items_2"): + 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}", []): + if not supervisor.can_deliver_training(item, depth): + self.add_error(f"items_{depth}", f"You are not permitted to deliver training in {item}") + return cleaned_data + def clean_date(self): return QualificationForm.clean_date(self) diff --git a/training/migrations/0009_trainingitem_technician_can_train.py b/training/migrations/0009_trainingitem_technician_can_train.py new file mode 100644 index 00000000..9f1327af --- /dev/null +++ b/training/migrations/0009_trainingitem_technician_can_train.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.17 on 2026-10-03 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('training', '0008_unique_constraints'), + ] + + operations = [ + migrations.AddField( + model_name='trainingitem', + name='technician_can_train', + field=models.BooleanField(default=False, help_text="Technicians in this item's department who are passed out on it may deliver training up to Training Complete"), + ), + ] diff --git a/training/models.py b/training/models.py index a7ec3f0f..7e8224d7 100644 --- a/training/models.py +++ b/training/models.py @@ -56,6 +56,18 @@ class Trainee(Profile, RevisionMixin): def is_driver(self): 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.""" + 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) + def get_records_of_depth(self, depth): return self.qualifications_obtained.filter(depth=depth).select_related("item", "trainee", "supervisor") @@ -109,6 +121,10 @@ class TrainingItem(models.Model): name = models.CharField(max_length=50) description = models.TextField(blank=True) active = models.BooleanField(default=True) + technician_can_train = models.BooleanField( + default=False, + help_text="Technicians in this item's department who are passed out on it may deliver training up to Training Complete", + ) prerequisites = models.ManyToManyField("self", symmetrical=False, blank=True) objects = TrainingItemManager() @@ -210,7 +226,9 @@ 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.item.category.training_level: + 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" diff --git a/training/templates/base_training.html b/training/templates/base_training.html index 6a19ca85..7b90ba7f 100644 --- a/training/templates/base_training.html +++ b/training/templates/base_training.html @@ -30,7 +30,7 @@ Item List - {% if request.user.is_supervisor %} + {% if request.user.can_log_training %}