Repository: carpentries/amy

Issue:
Require email reschedule date/time to be in the future
Currently past date/time values are allowed. 


Discussion:

Resolving patch:
diff --git a/amy/emails/forms.py b/amy/emails/forms.py
--- a/amy/emails/forms.py
+++ b/amy/emails/forms.py
@@ -1,3 +1,5 @@
+from datetime import UTC, datetime
+
 from django import forms
 from markdownx.fields import MarkdownxFormField
 
@@ -87,6 +89,14 @@ class ScheduledEmailRescheduleForm(forms.Form):
 
     helper = BootstrapHelper(submit_label="Update")
 
+    def clean_scheduled_at(self):
+        scheduled_at = self.cleaned_data["scheduled_at"]
+
+        if scheduled_at < datetime.now(tz=UTC):
+            raise forms.ValidationError("Scheduled time cannot be in the past.")
+
+        return scheduled_at
+
 
 class ScheduledEmailCancelForm(forms.Form):
     confirm = forms.CharField(required=False)


Test patch:
diff --git a/amy/emails/tests/test_forms.py b/amy/emails/tests/test_forms.py
new file mode 100644
--- /dev/null
+++ b/amy/emails/tests/test_forms.py
@@ -0,0 +1,44 @@
+from datetime import UTC, datetime
+from unittest.mock import MagicMock, patch
+
+from django.test import TestCase
+
+from emails.forms import ScheduledEmailRescheduleForm
+
+
+class TestScheduledEmailRescheduleForm(TestCase):
+    @patch("emails.forms.datetime", wraps=datetime)
+    def test_clean_scheduled_at_success(self, mock_datetime: MagicMock) -> None:
+        # Arrange
+        mock_datetime.now.return_value = datetime(2023, 1, 1, 0, 0, 0, tzinfo=UTC)
+        data = {
+            "scheduled_at_0": "2024-01-01",
+            "scheduled_at_1": "00:00",
+        }
+        form = ScheduledEmailRescheduleForm(data=data)
+
+        # Act
+        result = form.is_valid()
+
+        # Assert
+        self.assertTrue(result)
+
+    @patch("emails.forms.datetime", wraps=datetime)
+    def test_clean_scheduled_at_failure(self, mock_datetime: MagicMock) -> None:
+        # Arrange
+        mock_datetime.now.return_value = datetime(2024, 1, 1, 0, 0, 0, tzinfo=UTC)
+        data = {
+            "scheduled_at_0": "2023-01-01",
+            "scheduled_at_1": "00:00",
+        }
+        form = ScheduledEmailRescheduleForm(data=data)
+
+        # Act
+        result = form.is_valid()
+
+        # Assert
+        self.assertFalse(result)
+        self.assertEqual(
+            form.errors["scheduled_at"],
+            ["Scheduled time cannot be in the past."],
+        )
diff --git a/amy/emails/tests/test_views.py b/amy/emails/tests/test_views.py
--- a/amy/emails/tests/test_views.py
+++ b/amy/emails/tests/test_views.py
@@ -1,4 +1,5 @@
 from datetime import UTC, datetime, timedelta
+from unittest.mock import MagicMock, patch
 
 from django.test import RequestFactory, override_settings
 from django.urls import reverse
@@ -504,8 +505,9 @@ def test_disallowed_email_statuses(self) -> None:
 
 
 class TestScheduledEmailReschedule(TestBase):
+    @patch("emails.forms.datetime", wraps=datetime)
     @override_settings(FLAGS={"EMAIL_MODULE": [("boolean", True)]})
-    def test_view(self) -> None:
+    def test_view(self, mock_datetime: MagicMock) -> None:
         # Arrange
         super()._setUpUsersAndLogin()
         template = EmailTemplate.objects.create(
@@ -531,6 +533,7 @@ def test_view(self) -> None:
             template=template,
         )
         url = reverse("scheduledemail_reschedule", kwargs={"pk": scheduled_email.pk})
+        mock_datetime.now.return_value = datetime(2022, 12, 31, 23, 59, 59, tzinfo=UTC)
         new_scheduled_date = datetime(2023, 1, 1, 0, 0, tzinfo=UTC)
         data = {
             "scheduled_at_0": f"{new_scheduled_date:%Y-%m-%d}",