Repository: carpentries/amy

Issue:
Update view for unevaluated contributions 
This view lets us see people with unevaluated contributions. This should be updated to show people with unevaluated "Get Involved" step.

https://amy.carpentries.org/trainings/trainees/?search=&lesson_contribution=on&is_instructor=&training=&training_request=unknown&order_by=

Created automatically from an Asana task.


Discussion:

Resolving patch:
diff --git a/amy/trainings/filters.py b/amy/trainings/filters.py
--- a/amy/trainings/filters.py
+++ b/amy/trainings/filters.py
@@ -37,8 +37,8 @@ def filter_trainees_by_trainee_name_or_email(queryset, name, value):
         return queryset
 
 
-def filter_trainees_by_unevaluated_lesson_contribution_presence(queryset, name, flag):
-    if flag:  # return only trainees with an unevaluated lesson contribution
+def filter_trainees_by_unevaluated_get_involved_presence(queryset, name, flag):
+    if flag:  # return only trainees with an unevaluated Get Involved submission
         return queryset.filter(trainingprogress__state="n").distinct()
     else:
         return queryset
@@ -87,10 +87,10 @@ class TraineeFilter(AMYFilterSet):
         widget=widgets.CheckboxInput,
     )
 
-    lesson_contribution = django_filters.BooleanFilter(
-        label="Only trainees with unevaluated Lesson Contribution",
+    get_involved = django_filters.BooleanFilter(
+        label="Only trainees with unevaluated Get Involved submission",
         widget=widgets.CheckboxInput,
-        method=filter_trainees_by_unevaluated_lesson_contribution_presence,
+        method=filter_trainees_by_unevaluated_get_involved_presence,
     )
 
     training_request = django_filters.BooleanFilter(
@@ -131,7 +131,7 @@ class Meta:
         fields = [
             "search",
             "all_persons",
-            "lesson_contribution",
+            "get_involved",
             "is_instructor",
             "training",
         ]
diff --git a/amy/workshops/templatetags/training_progress.py b/amy/workshops/templatetags/training_progress.py
--- a/amy/workshops/templatetags/training_progress.py
+++ b/amy/workshops/templatetags/training_progress.py
@@ -36,11 +36,17 @@ def progress_description(progress):
     else:
         involvement = ""
 
+    day = (
+        progress.date.strftime("%A %d %B %Y")
+        if progress.date
+        else progress.created_at.strftime("%A %d %B %Y at %H:%M")
+    )
+
     text = "{state} {type}{involvement}<br />on {day}.{notes}".format(
         state=progress.get_state_display(),
         type=progress.requirement,
         involvement=involvement,
-        day=progress.created_at.strftime("%A %d %B %Y at %H:%M"),
+        day=day,
         notes="<br />Notes: {}".format(escape(progress.notes))
         if progress.notes
         else "",


Test patch:
diff --git a/amy/trainings/tests/test_filters.py b/amy/trainings/tests/test_filters.py
new file mode 100644
--- /dev/null
+++ b/amy/trainings/tests/test_filters.py
@@ -0,0 +1,264 @@
+from datetime import datetime, timedelta
+
+from django.utils import timezone
+
+from trainings.filters import TraineeFilter
+from trainings.models import Involvement
+from trainings.views import all_trainees_queryset
+from workshops.models import (
+    Event,
+    Person,
+    Role,
+    Task,
+    TrainingProgress,
+    TrainingRequest,
+    TrainingRequirement,
+)
+from workshops.tests.base import TestBase
+
+
+class TestTraineeFilter(TestBase):
+    """
+    A test should exist for each filter listed in test_fields().
+    """
+
+    def setUp(self) -> None:
+        super().setUp()  # create some persons
+        self._setUpEvents()  # create some events
+        self._setUpRoles()  # create learner role
+
+        self.model = Person
+
+        self.welcome = TrainingRequirement.objects.create(
+            name="Welcome Session", url_required=False, event_required=False
+        )
+        self.event_required = TrainingRequirement.objects.create(
+            name="Training", url_required=False, event_required=True
+        )
+        self.get_involved = TrainingRequirement.objects.create(
+            name="Get Involved",
+            url_required=False,
+            event_required=False,
+            involvement_required=True,
+        )
+        self.url_and_date_required, _ = Involvement.objects.get_or_create(
+            name="GitHub Contribution",
+            defaults={
+                "display_name": "GitHub Contribution",
+                "url_required": True,
+                "date_required": True,
+            },
+        )
+        self.notes_required, _ = Involvement.objects.get_or_create(
+            name="Other without date",
+            defaults={
+                "display_name": "Other without date",
+                "notes_required": True,
+                "date_required": False,
+            },
+        )
+
+        # add training progress for Spiderman
+        # training request, event task, and welcome session
+        self.training_request = TrainingRequest.objects.create(
+            person=self.spiderman,
+            review_process="open",
+            personal="Peter",
+            family="Parker",
+            email="peter@webslinger.net",
+            state="a",
+        )
+        self.event = Event.objects.create(
+            start=datetime.today() + timedelta(days=-31),
+            end=datetime.today() + timedelta(days=-30),
+            slug="training-event-ttt",
+            host=self.org_alpha,
+            url="http://example.org/training-event-ttt",
+            country="US",
+            venue="School",
+            address="Overthere",
+            latitude=1,
+            longitude=2,
+        )
+        Task.objects.create(
+            event=self.event,
+            person=self.spiderman,
+            role=Role.objects.get(name="learner"),
+        )
+        self.progress1 = TrainingProgress.objects.create(
+            requirement=self.welcome,
+            trainee=self.spiderman,
+            state="p",
+        )
+
+        # add some Get Involved progress to another trainee
+        # (pretending that it was submitted by them)
+        self.progress2 = TrainingProgress.objects.create(
+            requirement=self.get_involved,
+            involvement_type=self.url_and_date_required,
+            trainee=self.ironman,
+            state="n",
+            date=datetime(2023, 5, 31),
+            url="https://example.com",
+        )
+
+        # add progress for a trainee who has their instructor badge
+        self.progress3 = TrainingProgress.objects.create(
+            requirement=self.welcome,
+            trainee=self.hermione,
+            state="p",
+        )
+
+        # get filterset
+        self.filterset = TraineeFilter({})
+
+        # use a queryset where trainee annotations are available (e.g. is_instructor)
+        # this still includes all Persons
+        self.qs = all_trainees_queryset()
+
+    def tearDown(self) -> None:
+        # clean up progresses
+        self.progress1.delete()
+        self.progress2.delete()
+        self.progress3.delete()
+        self.training_request.delete()
+
+        super().tearDown()
+
+    def test_fields(self):
+        # Arrange & Act stages happen in setUp()
+        # Assert
+        self.assertEqual(
+            set(self.filterset.filters.keys()),
+            {
+                "search",
+                "all_persons",
+                "get_involved",
+                "training_request",
+                "is_instructor",
+                "training",
+                "order_by",
+            },
+        )
+
+    def test_filter_search_name(self):
+        # Arrange
+        filter_name = "search"
+        value = "Peter Parker"
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertQuerysetEqual(result, [self.spiderman])
+
+    def test_filter_search_email(self):
+        # Arrange
+        filter_name = "search"
+        value = "peter@webslinger.net"
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertQuerysetEqual(result, [self.spiderman])
+
+    def test_filter_all_persons(self):
+        # Arrange
+        filter_name = "all_persons"
+        value = True
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertQuerysetEqual(result, Person.objects.all())
+
+    def test_filter_get_involved(self):
+        # Arrange
+        filter_name = "get_involved"
+        value = True
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertQuerysetEqual(result, [self.ironman])
+
+    def test_filter_training_request_true(self):
+        # Arrange
+        filter_name = "training_request"
+        value = True
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertIn(self.spiderman, result)
+        self.assertNotIn(self.ironman, result)
+        self.assertQuerysetEqual(result, [self.spiderman])
+
+    def test_filter_training_request_false(self):
+        # Arrange
+        filter_name = "training_request"
+        value = False
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertIn(self.ironman, result)
+        self.assertIn(self.hermione, result)
+        self.assertNotIn(self.spiderman, result)
+
+    def test_filter_is_instructor(self):
+        # Arrange
+        filter_name = "is_instructor"
+        value = "no"
+
+        # Act
+        result = self.filterset.filters[filter_name].filter(self.qs, value)
+
+        # Assert
+        self.assertIn(self.spiderman, result)
+        self.assertIn(self.ironman, result)
+
+    def test_filter_training(self):
+        # Arrange
+        name = "training"
+        value = self.event.pk
+
+        # Act
+        result = self.filterset.filters[name].filter(self.qs, value)
+
+        # Assert
+        self.assertQuerysetEqual(result, [self.spiderman])
+
+    def test_filter_order_by(self):
+        # Arrange
+        filter_name = "order_by"
+        fields = self.filterset.filters[filter_name].param_map
+        qs = self.qs.filter(pk__in=[self.spiderman.pk, self.ironman.pk])
+        results = {}
+        # none of these users have logged in, so manually set
+        self.spiderman.last_login = datetime(2023, 4, 1, 0, 0, 0, tzinfo=timezone.utc)
+        self.spiderman.save()
+        self.ironman.last_login = datetime(2023, 5, 1, 0, 0, 0, tzinfo=timezone.utc)
+        self.ironman.save()
+
+        # default ordering is ascending
+        expected_results = {
+            "last_login": [self.spiderman, self.ironman],
+            "email": [self.ironman, self.spiderman],
+        }
+
+        # Act
+        for field in fields.keys():
+            results[field] = self.filterset.filters[filter_name].filter(qs, [field])
+
+        # Assert
+        # we don't have any unexpected fields
+        self.assertEqual(fields.keys(), expected_results.keys())
+        # each field was filtered correctly
+        for field in fields.keys():
+            self.assertQuerysetEqual(results[field], expected_results[field])