Repository: carpentries/amy

Issue:
Require dates for role
For all community roles, dates should be required.

If inactive, both the start and end date should be required. ~~Additionally, the current date can not be in between the start and end dates.~~
If active, the start date should be required.  The end date should display as `present` rather than as `???`.

EDIT @pbanaszkiewicz to reflect that the end date can be in the future.  


Discussion:
@maneesha thank you for the update!

Resolving patch:
diff --git a/amy/communityroles/forms.py b/amy/communityroles/forms.py
--- a/amy/communityroles/forms.py
+++ b/amy/communityroles/forms.py
@@ -1,4 +1,5 @@
 from collections import defaultdict
+from datetime import date
 from typing import Any, Optional, Union
 
 from django import forms
@@ -9,7 +10,7 @@
 from workshops.models import Award, Person
 
 from .fields import CustomKeysJSONField
-from .models import CommunityRole, CommunityRoleConfig
+from .models import CommunityRole, CommunityRoleConfig, CommunityRoleInactivation
 
 
 class CommunityRoleForm(WidgetOverrideMixin, forms.ModelForm):
@@ -77,6 +78,10 @@ def clean(self) -> Union[dict[str, Any], None]:
         config: Optional[CommunityRoleConfig] = cleaned_data.get("config")
         award: Optional[Award] = cleaned_data.get("award")
         person: Optional[Person] = cleaned_data.get("person")
+        inactivation: Optional[CommunityRoleInactivation] = cleaned_data.get(
+            "inactivation"
+        )
+        end_date: Optional[date] = cleaned_data.get("end")
 
         # Config is required, but field validation for 'config' should raise
         # validation error first.
@@ -134,6 +139,12 @@ def clean(self) -> Union[dict[str, Any], None]:
                     )
                 )
 
+        # End date is required when any inactivation was selected.
+        if inactivation is not None and end_date is None:
+            errors["end"].append(
+                ValidationError("Required when Reason for inactivation selected.")
+            )
+
         if errors:
             raise ValidationError(errors)
 
diff --git a/amy/communityroles/migrations/0005_alter_communityrole_start.py b/amy/communityroles/migrations/0005_alter_communityrole_start.py
new file mode 100644
--- /dev/null
+++ b/amy/communityroles/migrations/0005_alter_communityrole_start.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.14 on 2022-09-17 03:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('communityroles', '0004_communityrole_field_verbose_names'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='communityrole',
+            name='start',
+            field=models.DateField(null=True, verbose_name='Role Start Date'),
+        ),
+    ]
diff --git a/amy/communityroles/models.py b/amy/communityroles/models.py
--- a/amy/communityroles/models.py
+++ b/amy/communityroles/models.py
@@ -76,7 +76,7 @@ class CommunityRole(CreatedUpdatedMixin, models.Model):
         blank=True,
         verbose_name="Associated Award",
     )
-    start = models.DateField(null=True, blank=True, verbose_name="Role Start Date")
+    start = models.DateField(null=True, blank=False, verbose_name="Role Start Date")
     end = models.DateField(null=True, blank=True, verbose_name="Role End Date")
     inactivation = models.ForeignKey(
         CommunityRoleInactivation,


Test patch:
diff --git a/amy/communityroles/tests/test_community_role_forms.py b/amy/communityroles/tests/test_community_role_forms.py
--- a/amy/communityroles/tests/test_community_role_forms.py
+++ b/amy/communityroles/tests/test_community_role_forms.py
@@ -3,7 +3,11 @@
 from django.contrib.contenttypes.models import ContentType
 
 from communityroles.forms import CommunityRoleForm, CommunityRoleUpdateForm
-from communityroles.models import CommunityRole, CommunityRoleConfig
+from communityroles.models import (
+    CommunityRole,
+    CommunityRoleConfig,
+    CommunityRoleInactivation,
+)
 from fiscal.models import Membership
 from workshops.models import Award, Badge, Lesson, Person
 from workshops.tests.base import TestBase
@@ -66,9 +70,10 @@ def test_empty_payload(self):
 
         # Assert
         self.assertFalse(form.is_valid())  # errors expected
-        self.assertEqual(form.errors.keys(), {"config", "person"})
+        self.assertEqual(form.errors.keys(), {"config", "person", "start"})
         self.assertEqual(form.errors["config"], ["This field is required."])
         self.assertEqual(form.errors["person"], ["This field is required."])
+        self.assertEqual(form.errors["start"], ["This field is required."])
 
     def test_award_required(self):
         # Arrange
@@ -325,6 +330,44 @@ def test_generic_relation_object_doesnt_exist(self):
             ["Generic relation object of model Lesson doesn't exist"],
         )
 
+    def test_end_date_required_when_inactivation_selected(self):
+        """Should not validate if the inactivation reason is provided and end date
+        is missing."""
+        # Arrange
+        ct = ContentType.objects.get_for_model(Lesson)
+        inactivation = CommunityRoleInactivation.objects.create(name="End of term")
+        test_config = CommunityRoleConfig.objects.create(
+            name="test",
+            display_name="Test",
+            link_to_award=True,
+            award_badge_limit=None,
+            link_to_membership=True,
+            additional_url=True,
+            generic_relation_content_type=ct,
+        )
+        data = {
+            "config": test_config.pk,
+            "person": self.hermione.pk,
+            "award": self.award.pk,
+            "start": "2021-11-14",
+            "end": None,
+            "inactivation": inactivation.pk,
+            "membership": self.membership.pk,
+            "url": "https://example.org",
+            "generic_relation_content_type": ct.pk,
+            "generic_relation_pk": self.git.pk,  # Lesson instance
+        }
+
+        # Act
+        form = CommunityRoleForm(data)
+
+        # Assert
+        self.assertFalse(form.is_valid())
+        self.assertEqual(form.errors.keys(), {"end"})
+        self.assertEqual(
+            form.errors["end"], ["Required when Reason for inactivation selected."]
+        )
+
 
 class TestCommunityRoleUpdateForm(TestBase):
     def test_fields(self) -> None:
@@ -411,8 +454,9 @@ def test_empty_payload(self) -> None:
 
         # Assert
         self.assertFalse(form.is_valid())  # errors expected
-        self.assertEqual(form.errors.keys(), {"person"})
+        self.assertEqual(form.errors.keys(), {"person", "start"})
         self.assertEqual(form.errors["person"], ["This field is required."])
+        self.assertEqual(form.errors["start"], ["This field is required."])
 
     def test_clean_success(self) -> None:
         # Arrange
@@ -431,7 +475,7 @@ def test_clean_success(self) -> None:
             "config": config.pk,
             "person": person.pk,
             "award": "",
-            "start": "",
+            "start": "2022-09-17",
             "end": "",
             "inactivation": None,
             "membership": "",
@@ -466,7 +510,7 @@ def test_custom_keys_field(self) -> None:
             "config": config.pk,
             "person": person.pk,
             "award": "",
-            "start": "",
+            "start": "2022-09-17",
             "end": "",
             "inactivation": None,
             "membership": "",