Repository: carpentries/amy

Issue:
Update requirements for checkout 
Initial description:
Currently, someone who has completed the discussion, demo, and contribution is listed as eligible for an instructor badge. This should change to having completed the welcome session (renamed from discussion), demo, and get involved (new field, removing contribution)

I think right now that the Person has to have a learner task at a TTT event to be able to assign these requirements to them.  https://app.asana.com/0/412899495476717/list do we want to change this so that they have to have passed a TTT event? 

After discussion:
Preference is actually to make the system more permissive such that we can assign checkout tasks to anyone (regardless of whether they have a TTT event task at all), while (only) eligibility for a badge requires a passed TTT event. No time cut off is needed - a trainee could theoretically complete the welcome session/demo/get involved steps long before doing their training. (Eli note: this might mean we have to think about how to create Person records from those other steps)

Created automatically from an Asana task.


Discussion:

Resolving patch:
diff --git a/amy/trainings/forms.py b/amy/trainings/forms.py
--- a/amy/trainings/forms.py
+++ b/amy/trainings/forms.py
@@ -1,6 +1,5 @@
 from crispy_forms.layout import Layout
 from django import forms
-from django.core.exceptions import ValidationError
 from django.forms import CharField, RadioSelect, TextInput
 
 from trainings.models import Involvement
@@ -79,28 +78,6 @@ class Meta:
     class Media:
         js = ("trainingprogress_form.js",)
 
-    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."
-            )
-
-        errors = dict()
-
-        # TODO: validation based on url_required in Involvement, etc
-
-        # raise errors if any present
-        if errors:
-            raise ValidationError(errors)
-
 
 class BulkAddTrainingProgressForm(forms.ModelForm):
     event = forms.ModelChoiceField(
@@ -148,19 +125,3 @@ class Meta:
             "state": RadioSelect,
             "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."
-                )
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -658,11 +658,9 @@ def passed_either(*reqs):
                 )
             )
 
-        LESSON_CONTRIBUTION_NAMES = ["Lesson Contribution"]
-
         return self.annotate(
             passed_training=passed("Training"),
-            passed_lesson_contribution=passed_either(*LESSON_CONTRIBUTION_NAMES),
+            passed_get_involved=passed("Get Involved"),
             passed_welcome=passed("Welcome Session"),
             passed_demo=passed("Demo"),
         ).annotate(
@@ -673,7 +671,7 @@ def passed_either(*reqs):
             instructor_eligible=(
                 F("passed_training")
                 * F("passed_welcome")
-                * F("passed_lesson_contribution")
+                * F("passed_get_involved")
                 * F("passed_demo")
             )
         )
@@ -970,7 +968,7 @@ def get_missing_instructor_requirements(self):
         """
         fields = [
             ("passed_training", "Training"),
-            ("passed_lesson_contribution", "Lesson Contribution"),
+            ("passed_get_involved", "Get Involved"),
             ("passed_welcome", "Welcome Session"),
             ("passed_demo", "Demo"),
         ]


Test patch:
diff --git a/amy/dashboard/tests/test_instructor_dashboard.py b/amy/dashboard/tests/test_instructor_dashboard.py
--- a/amy/dashboard/tests/test_instructor_dashboard.py
+++ b/amy/dashboard/tests/test_instructor_dashboard.py
@@ -2,6 +2,7 @@
 
 from django.urls import reverse
 
+from trainings.models import Involvement
 from workshops.models import Award, Person, TrainingProgress, TrainingRequirement
 from workshops.tests.base import TestBase
 
@@ -57,19 +58,30 @@ def test_neither_swc_nor_dc_instructor(self):
         self.assertNotContains(rv, "Congratulations, you're certified")
 
     def test_eligible_but_not_awarded(self):
-        """Test what is dispslayed when a trainee is eligible to be certified
-        as an SWC/DC Instructor, but doesn't have appropriate badge awarded
+        """Test what is displayed when a trainee is eligible to be certified
+        as an Instructor, but doesn't have appropriate badge awarded
         yet."""
         requirements = [
             "Training",
-            "Lesson Contribution",
+            "Get Involved",
             "Welcome Session",
             "Demo",
         ]
         for requirement in requirements:
+            if requirement == "Get Involved":
+                involvement = Involvement.objects.get(name="GitHub Contribution")
+                date = datetime.today()
+                url = "https://example.com"
+            else:
+                involvement = None
+                date = None
+                url = None
             TrainingProgress.objects.create(
                 trainee=self.admin,
                 requirement=TrainingRequirement.objects.get(name=requirement),
+                involvement_type=involvement,
+                date=date,
+                url=url,
             )
 
         admin = Person.objects.annotate_with_instructor_eligibility().get(
diff --git a/amy/trainings/tests/test_trainees.py b/amy/trainings/tests/test_trainees.py
--- a/amy/trainings/tests/test_trainees.py
+++ b/amy/trainings/tests/test_trainees.py
@@ -4,6 +4,7 @@
 from django.urls import reverse
 
 from trainings.filters import filter_trainees_by_instructor_status
+from trainings.models import Involvement
 from trainings.views import all_trainees_queryset
 from workshops.models import (
     Award,
@@ -94,12 +95,15 @@ def _setUpTrainingRequirements(self):
         self.demo, _ = TrainingRequirement.objects.get_or_create(
             name="Demo", defaults={"url_required": True}
         )
-        self.lesson_contribution, _ = TrainingRequirement.objects.get_or_create(
-            name="Lesson Contribution", defaults={}
+        self.get_involved, _ = TrainingRequirement.objects.get_or_create(
+            name="Get Involved", defaults={}
         )
 
         self.welcome = TrainingRequirement.objects.get(name="Welcome Session")
         self.training = TrainingRequirement.objects.get(name="Training")
+        self.involvement, _ = Involvement.objects.get_or_create(
+            name="Test Involvement", defaults={}
+        )
 
     def _setUpInstructors(self):
         # prepare data
@@ -160,7 +164,9 @@ def _setUpInstructors(self):
                 ),
                 TrainingProgress(
                     trainee=self.trainee1,
-                    requirement=self.lesson_contribution,
+                    requirement=self.get_involved,
+                    involvement_type=self.involvement,
+                    date=date(2023, 6, 1),
                     state="p",
                 ),
                 TrainingProgress(
@@ -191,7 +197,9 @@ def _setUpInstructors(self):
                 ),
                 TrainingProgress(
                     trainee=self.trainee2,
-                    requirement=self.lesson_contribution,
+                    requirement=self.get_involved,
+                    involvement_type=self.involvement,
+                    date=date(2023, 6, 1),
                     state="p",
                 ),
                 TrainingProgress(
@@ -226,7 +234,9 @@ def _setUpInstructors(self):
                 ),
                 TrainingProgress(
                     trainee=self.trainee3,
-                    requirement=self.lesson_contribution,
+                    requirement=self.get_involved,
+                    involvement_type=self.involvement,
+                    date=date(2023, 6, 1),
                     state="p",
                 ),
                 TrainingProgress(
@@ -295,7 +305,7 @@ def test_eligibility_query(self):
                 is_instructor=0,
                 passed_training=1,
                 passed_welcome=1,
-                passed_lesson_contribution=1,
+                passed_get_involved=1,
                 passed_demo=1,
                 instructor_eligible=1,
             ),
@@ -305,7 +315,7 @@ def test_eligibility_query(self):
                 is_instructor=4,
                 passed_training=1,
                 passed_welcome=1,
-                passed_lesson_contribution=1,
+                passed_get_involved=1,
                 passed_demo=1,
                 instructor_eligible=1,
             ),
@@ -315,7 +325,7 @@ def test_eligibility_query(self):
                 is_instructor=0,
                 passed_training=1,
                 passed_welcome=0,
-                passed_lesson_contribution=1,
+                passed_get_involved=1,
                 passed_demo=1,
                 instructor_eligible=0,
             ),
diff --git a/amy/trainings/tests/test_training_progress.py b/amy/trainings/tests/test_training_progress.py
--- a/amy/trainings/tests/test_training_progress.py
+++ b/amy/trainings/tests/test_training_progress.py
@@ -263,29 +263,15 @@ def test_progress_with_failed_status_requires_notes(self):
         with self.assertValidationErrors(["notes"]):
             failed_progress_no_notes.full_clean()
 
-    def test_form_invalid_if_trainee_has_no_training_task(self):
+    def test_form_valid_if_trainee_has_no_training_task(self):
+        """Regression test for https://github.com/carpentries/amy/issues/2440"""
         data = {
             "requirement": self.requirement.pk,
             "state": "p",
             "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]
-        )
-
-    def test_form_with_failed_status_requires_notes(self):
-        data = {
-            "requirement": self.requirement.pk,
-            "state": "p",
-            "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]
-        )
+        self.assertEqual(form.is_valid(), True)
 
 
 class TestProgressLabelTemplateTag(TestBase):
diff --git a/amy/workshops/tests/test_person.py b/amy/workshops/tests/test_person.py
--- a/amy/workshops/tests/test_person.py
+++ b/amy/workshops/tests/test_person.py
@@ -14,6 +14,7 @@
 from webtest.forms import Upload
 
 from consents.models import Consent, Term, TermEnum
+from trainings.models import Involvement
 from workshops.filters import filter_taught_workshops
 from workshops.forms import PersonForm, PersonsMergeForm
 from workshops.mixins import GenderMixin
@@ -1292,20 +1293,27 @@ class TestGetMissingInstructorRequirements(TestBase):
     def setUp(self):
         self.person = Person.objects.create(username="person")
         self.training = TrainingRequirement.objects.get(name="Training")
-        self.lesson_contribution, _ = TrainingRequirement.objects.get_or_create(
-            name="Lesson Contribution", defaults={"url_required": True}
+        self.get_involved, _ = TrainingRequirement.objects.get_or_create(
+            name="Get Involved", defaults={"involvement_required": True}
         )
         self.welcome = TrainingRequirement.objects.get(name="Welcome Session")
         self.demo, _ = TrainingRequirement.objects.get_or_create(
             name="Demo", defaults={}
         )
+        self.involvement, _ = Involvement.objects.get_or_create(
+            name="Test Involvement", defaults={}
+        )
 
     def test_all_requirements_satisfied(self):
         TrainingProgress.objects.create(
             trainee=self.person, state="p", requirement=self.training
         )
         TrainingProgress.objects.create(
-            trainee=self.person, state="p", requirement=self.lesson_contribution
+            trainee=self.person,
+            state="p",
+            requirement=self.get_involved,
+            involvement_type=self.involvement,
+            date=date(2023, 5, 1),
         )
         TrainingProgress.objects.create(
             trainee=self.person, state="p", requirement=self.welcome
@@ -1320,12 +1328,20 @@ def test_all_requirements_satisfied(self):
         self.assertEqual(person.get_missing_instructor_requirements(), [])
 
     def test_some_requirements_are_fulfilled(self):
-        # Lesson Contribution was accepted, the second time.
+        # Get Involved was accepted, the second time.
         TrainingProgress.objects.create(
-            trainee=self.person, state="f", requirement=self.lesson_contribution
+            trainee=self.person,
+            state="f",
+            requirement=self.get_involved,
+            involvement_type=self.involvement,
+            date=date(2023, 5, 1),
         )
         TrainingProgress.objects.create(
-            trainee=self.person, state="p", requirement=self.lesson_contribution
+            trainee=self.person,
+            state="p",
+            requirement=self.get_involved,
+            involvement_type=self.involvement,
+            date=date(2023, 6, 1),
         )
         # Not passed progress should be ignored.
         TrainingProgress.objects.create(
@@ -1349,7 +1365,7 @@ def test_none_requirement_is_fulfilled(self):
         )
         self.assertEqual(
             person.get_missing_instructor_requirements(),
-            ["Training", "Lesson Contribution", "Welcome Session", "Demo"],
+            ["Training", "Get Involved", "Welcome Session", "Demo"],
         )