Repository: carpentries/amy

Issue:
Assigned training should be required in order to have any training progress
If someone has not been  a learner at a TTT event, we should not be able to assign any training progress steps to them.

See example: https://amy.software-carpentry.org/workshops/person/13211/


Discussion:

Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -1359,6 +1359,18 @@ class Meta:
             'state': RadioSelect,
         }
 
+    def clean(self):
+        cleaned_data = super().clean()
+
+        trainee = cleaned_data.get('trainee')
+
+        # check if trainee has at least one training task
+        training_tasks = trainee.get_training_tasks()
+
+        if not training_tasks:
+            raise ValidationError("It's not possible to add training progress "
+                                  "to a trainee without any training task.")
+
 
 class BulkAddTrainingProgressForm(forms.ModelForm):
     event = forms.ModelChoiceField(
@@ -1407,6 +1419,20 @@ class Meta:
             'notes': TextInput,
         }
 
+    def clean(self):
+        cleaned_data = super().clean()
+
+        trainees = cleaned_data.get('trainees')
+
+        # check if all trainees have at least one training task
+        for trainee in trainees:
+            training_tasks = trainee.get_training_tasks()
+
+            if not training_tasks:
+                raise ValidationError("It's not possible to add training "
+                                      "progress to a trainee without any "
+                                      "training task.")
+
 
 class BulkDiscardProgressesForm(forms.Form):
     """Form used to bulk discard all TrainingProgresses associated with


Test patch:
diff --git a/workshops/test/test_trainees.py b/workshops/test/test_trainees.py
--- a/workshops/test/test_trainees.py
+++ b/workshops/test/test_trainees.py
@@ -1,6 +1,15 @@
+from datetime import datetime
+
 from django.urls import reverse
 
-from workshops.models import TrainingProgress, TrainingRequirement
+from workshops.models import (
+    TrainingProgress,
+    TrainingRequirement,
+    Event,
+    Organization,
+    Tag,
+    Role,
+)
 from workshops.test.base import TestBase
 
 
@@ -9,11 +18,20 @@ def setUp(self):
         self._setUpUsersAndLogin()
         self._setUpAirports()
         self._setUpNonInstructors()
+        self._setUpTags()
+        self._setUpRoles()
 
         self.training = TrainingRequirement.objects.get(name='Training')
         self.homework = TrainingRequirement.objects.get(name='SWC Homework')
         self.discussion = TrainingRequirement.objects.get(name='Discussion')
 
+        self.ttt_event = Event.objects.create(
+            start=datetime(2018, 7, 14),
+            slug='2018-07-14-training',
+            host=Organization.objects.first(),
+        )
+        self.ttt_event.tags.add(Tag.objects.get(name='TTT'))
+
     def test_view_loads(self):
         rv = self.client.get(reverse('all_trainees'))
         self.assertEqual(rv.status_code, 200)
@@ -27,6 +45,18 @@ def test_bulk_add_progress(self):
             'state': 'f',
             'submit': '',
         }
+
+        # all trainees need to have a training task to assign a training
+        # progress to them
+        self.ironman.task_set.create(
+            event=self.ttt_event,
+            role=Role.objects.get(name='learner'),
+        )
+        self.spiderman.task_set.create(
+            event=self.ttt_event,
+            role=Role.objects.get(name='learner'),
+        )
+
         rv = self.client.post(reverse('all_trainees'), data, follow=True)
 
         self.assertEqual(rv.resolver_match.view_name, 'all_trainees')
diff --git a/workshops/test/test_training_progress.py b/workshops/test/test_training_progress.py
--- a/workshops/test/test_training_progress.py
+++ b/workshops/test/test_training_progress.py
@@ -4,8 +4,15 @@
 from django.template import Context, Template
 from django.urls import reverse
 
-from workshops.models import TrainingProgress, TrainingRequirement, Event, Tag, \
-    Organization
+from workshops.forms import TrainingProgressForm
+from workshops.models import (
+    TrainingProgress,
+    TrainingRequirement,
+    Event,
+    Tag,
+    Organization,
+    Role,
+)
 from workshops.test import TestBase
 
 
@@ -14,6 +21,8 @@ class TestTrainingProgressValidation(TestBase):
 
     def setUp(self):
         self._setUpUsersAndLogin()
+        self._setUpAirports()
+        self._setUpNonInstructors()
 
         self.requirement = TrainingRequirement.objects.create(
             name='Discussion', url_required=False, event_required=False)
@@ -95,6 +104,18 @@ def test_unevaluated_progress_may_have_mentor_or_examiner_associated(self):
         p1.full_clean()
         p2.full_clean()
 
+    def test_form_invalid_if_trainee_has_no_training_task(self):
+        data = {
+            'requirement': self.requirement.pk,
+            'state': 'p',
+            'evaluated_by': self.admin.pk,
+            'trainee': self.ironman.pk,
+        }
+        form = TrainingProgressForm(data)
+        self.assertEqual(form.is_valid(), False)
+        self.assertIn("not possible to add training progress",
+                      form.non_field_errors()[0])
+
 
 class TestProgressLabelTemplateTag(TestBase):
     def test_passed(self):
@@ -201,6 +222,8 @@ def setUp(self):
         self._setUpUsersAndLogin()
         self._setUpAirports()
         self._setUpNonInstructors()
+        self._setUpTags()
+        self._setUpRoles()
 
         self.requirement = TrainingRequirement.objects.create(name='Discussion')
         self.progress = TrainingProgress.objects.create(
@@ -209,6 +232,12 @@ def setUp(self):
             evaluated_by=self.admin,
             trainee=self.ironman,
         )
+        self.ttt_event = Event.objects.create(
+            start=datetime(2018, 7, 14),
+            slug='2018-07-14-training',
+            host=Organization.objects.first(),
+        )
+        self.ttt_event.tags.add(Tag.objects.get(name='TTT'))
 
     def test_create_view_loads(self):
         rv = self.client.get(reverse('trainingprogress_add'))
@@ -230,6 +259,14 @@ def test_create_view_works(self):
             'evaluated_by': self.admin.pk,
             'trainee': self.ironman.pk,
         }
+
+        # in order to add training progress, the trainee needs to have
+        # a training task
+        self.ironman.task_set.create(
+            event=self.ttt_event,
+            role=Role.objects.get(name='learner'),
+        )
+
         rv = self.client.post(reverse('trainingprogress_add'), data,
                               follow=True)
         self.assertEqual(rv.status_code, 200)