Repository: carpentries/amy

Issue:
Community Roles: disallow creating `Task` for instructor/trainer, who has been inactivated
As an admin user, I want to be unable to create a trainer or instructor task for a person, whose `trainer` or `instructor` community role has been inactivated.

Acceptance criteria:
* adding instructor or trainer task to an event is blocked if related person's instructor or trainer community role is inactive
* adding instructor or trainer task to a person is blocked if related person's instructor or trainer community role is inactive
* creating instructor or trainer task is blocked if selected person's instructor or trainer community role is inactive


Discussion:
@pbanaszkiewicz  has this been completed?
@maneesha No, not yet. I'll work on it this week.
@maneesha This is what I'm implementing:
1. Adding instructor task is blocked if person has no active instructor community role
2. Adding instructor task to TTT event is blocked if person has no active trainer community role

The difference is that:
1. Person could potentially have multiple instructor or trainer community roles, and I'm checking if all of them are inactive
2. Acceptance criteria were defined as if either inactive instructor or inactive trainer community role would block adding a task. Instead I switched to checking for inactive instructor community role for instructor task, and inactive trainer community role for trainer task (instructor in a TTT event).

Resolving patch:
diff --git a/amy/workshops/forms.py b/amy/workshops/forms.py
--- a/amy/workshops/forms.py
+++ b/amy/workshops/forms.py
@@ -14,6 +14,7 @@
 from django_countries.fields import CountryField
 from markdownx.fields import MarkdownxFormField
 
+from communityroles.models import CommunityRole
 from dashboard.models import Continent
 
 # this is used instead of Django Autocomplete Light widgets
@@ -37,6 +38,7 @@
     Membership,
     Organization,
     Person,
+    Role,
     Tag,
     Task,
 )
@@ -664,6 +666,42 @@ def __init__(self, *args, **kwargs):
             )
         self.helper = BootstrapHelper(**bootstrap_kwargs)
 
+    def clean(self):
+        result = super().clean()
+        errors = dict()
+        person: Person = self.cleaned_data["person"]
+        role: Role = self.cleaned_data["role"]
+        event: Event = self.cleaned_data["event"]
+
+        # Check validity of person's community role
+        if role.name == "instructor":
+            # If event is TTT (Train The Trainers), then community role "trainer"
+            # corresponds to role "instructor"; otherwise it's "instructor" community
+            # role.
+            community_role_name = "instructor"
+            if event.administrator.domain == "carpentries.org":
+                community_role_name = "trainer"
+
+            person_community_roles = CommunityRole.objects.filter(
+                person=person, config__name__iexact=community_role_name
+            ).select_related("config")
+
+            no_active_role = not any(
+                role.is_active() for role in person_community_roles
+            )
+
+            if person_community_roles and no_active_role:
+                errors["role"] = ValidationError(
+                    f'{person} has inactive "{community_role_name}" community role(s) '
+                    'related to "{role.name}" task.'
+                )
+
+        # raise errors if any present
+        if errors:
+            raise ValidationError(errors)
+
+        return result
+
 
 class PersonForm(forms.ModelForm):
     airport = forms.ModelChoiceField(


Test patch:
diff --git a/amy/workshops/tests/test_forms.py b/amy/workshops/tests/test_forms.py
new file mode 100644
--- /dev/null
+++ b/amy/workshops/tests/test_forms.py
@@ -0,0 +1,166 @@
+from django.test import TestCase
+
+from communityroles.models import (
+    CommunityRole,
+    CommunityRoleConfig,
+    CommunityRoleInactivation,
+)
+from workshops.forms import TaskForm
+from workshops.models import Event, Organization, Person, Role
+
+
+class TestTaskForm(TestCase):
+    def setUp(self) -> None:
+        instructor_training_organization = Organization.objects.create(
+            domain="carpentries.org",
+            fullname="Instructor Training",
+        )
+        swc = Organization.objects.create(
+            domain="software-carpentry.org",
+            fullname="Software Carpentry",
+        )
+        host = Organization.objects.first()
+        self.event1 = Event.objects.create(
+            slug="test-event2",
+            host=host,
+            administrator=swc,
+        )
+        self.event2 = Event.objects.create(
+            slug="test-event1",
+            host=host,
+            administrator=instructor_training_organization,
+        )
+        self.person = Person.objects.create(
+            personal="Test", family="User", email="test@example.org"
+        )
+        role = Role.objects.create(name="instructor", verbose_name="Instructor")
+        self.payload = {
+            "event": None,
+            "person": self.person.pk,
+            "role": role.pk,
+        }
+        self.instructor_community_role_config = CommunityRoleConfig.objects.create(
+            name="instructor",
+            display_name="Instructor",
+            link_to_award=False,
+            link_to_membership=False,
+            additional_url=False,
+        )
+        self.trainer_community_role_config = CommunityRoleConfig.objects.create(
+            name="trainer",
+            display_name="Trainer",
+            link_to_award=False,
+            link_to_membership=False,
+            additional_url=False,
+        )
+        self.inactivation = CommunityRoleInactivation.objects.create(name="Test")
+
+    def test_clean__no_community_roles__instructor(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event1
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)
+
+    def test_clean__no_community_roles__trainer(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event2
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)
+
+    def test_clean__inactive_community_role__instructor(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event1
+        CommunityRole.objects.create(
+            config=self.instructor_community_role_config,
+            person=self.person,
+            inactivation=self.inactivation,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertFalse(result)
+        self.assertEqual(form.errors.keys(), {"role"})
+
+    def test_clean__inactive_community_role__trainer(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event2
+        CommunityRole.objects.create(
+            config=self.trainer_community_role_config,
+            person=self.person,
+            inactivation=self.inactivation,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertFalse(result)
+        self.assertEqual(form.errors.keys(), {"role"})
+
+    def test_clean__active_community_roles__instructor(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event1
+        CommunityRole.objects.create(
+            config=self.instructor_community_role_config,
+            person=self.person,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)
+
+    def test_clean__active_community_roles__trainer(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event2
+        CommunityRole.objects.create(
+            config=self.trainer_community_role_config,
+            person=self.person,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)
+
+    def test_clean__mixed_active_inactive_community_roles__instructor(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event1
+        CommunityRole.objects.create(
+            config=self.instructor_community_role_config,
+            person=self.person,
+        )
+        CommunityRole.objects.create(
+            config=self.instructor_community_role_config,
+            person=self.person,
+            inactivation=self.inactivation,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)
+
+    def test_clean__mixed_active_inactive_community_roles__trainer(self) -> None:
+        # Arrange
+        self.payload["event"] = self.event2
+        CommunityRole.objects.create(
+            config=self.trainer_community_role_config,
+            person=self.person,
+        )
+        CommunityRole.objects.create(
+            config=self.trainer_community_role_config,
+            person=self.person,
+            inactivation=self.inactivation,
+        )
+        form = TaskForm(self.payload)
+        # Act
+        result = form.is_valid()
+        # Assert
+        self.assertTrue(result)