diff --git a/src/coldfront_plugin_cloud/management/commands/register_allocation_alerts.py b/src/coldfront_plugin_cloud/management/commands/register_allocation_alerts.py new file mode 100644 index 00000000..aa0d8232 --- /dev/null +++ b/src/coldfront_plugin_cloud/management/commands/register_allocation_alerts.py @@ -0,0 +1,27 @@ +from datetime import datetime, timezone, timedelta + +from django.core.management.base import BaseCommand +from django_q.tasks import schedule, Schedule + + +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """ + Registers a django-q schedule that updates allocations based on set criterias. The schedule runs everyday + """ + + def handle(self, *args, **options): + date = datetime.now(timezone.utc) + timedelta(days=1) + date = date.replace( + hour=0, minute=0, second=0, microsecond=0 + ) # TODO: What time of day to run this job? + schedule( + "coldfront_plugin_cloud.tasks.remind_allocations_revoked", + schedule_type=Schedule.DAILY, + next_run=date, + ) diff --git a/src/coldfront_plugin_cloud/tasks.py b/src/coldfront_plugin_cloud/tasks.py index b04aa0e8..50942bed 100644 --- a/src/coldfront_plugin_cloud/tasks.py +++ b/src/coldfront_plugin_cloud/tasks.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime, timezone, timedelta import logging import time from string import Template @@ -8,6 +8,9 @@ AllocationUser, AllocationAttribute, ) +from coldfront.core.utils import mail +from coldfront.core.utils.common import import_from_settings + from coldfront_plugin_cloud import ( attributes, @@ -22,6 +25,12 @@ logger = logging.getLogger(__name__) +CENTER_BASE_URL = import_from_settings("CENTER_BASE_URL") +EMAIL_SENDER = import_from_settings("EMAIL_SENDER") + +REMINDER_SCHEDULE_DAYS = [30, 14, 7, 2] +EXPIRATION_REMINDER_TEMPLATE = """""" # TODO (Quan): To be filled by https://github.com/nerc-project/coldfront-plugin-cloud/issues/316 + def get_kc_client(): return kc_client.KeyCloakAPIClient() @@ -106,13 +115,13 @@ def add_user_to_allocation(allocation_user_pk): # Note(knikolla): This task may be executed at the same time as # activating an allocation, therefore it has to wait for the project # to finish creating. Maximum wait is 2 minutes. - time_start = datetime.datetime.utcnow() + time_start = datetime.utcnow() max_wait_seconds = 120 while not ( project_id := allocation.get_attribute(attributes.ALLOCATION_PROJECT_ID) ): - delta = datetime.datetime.utcnow() - time_start + delta = datetime.utcnow() - time_start if delta.seconds >= max_wait_seconds: raise Exception( f"Project not yet created after {delta.seconds} seconds." @@ -220,3 +229,36 @@ def remove_user_from_keycloak(allocation_user_pk): ) return kc_admin_client.remove_user_from_group(user_id, group_id) + + +def remind_allocations_revoked(): + today = datetime.now(timezone.utc) + reminder_dates = [today + timedelta(days=d) for d in REMINDER_SCHEDULE_DAYS] + status_to_remind = [ # TODO: Confirm these are the statuses we want alerted? + "Expired" + ] + + allocations_to_alert = Allocation.objects.filter( + status__name__in=status_to_remind, end_date__in=reminder_dates + ) + + for allocation in allocations_to_alert: + pi_email = allocation.project.pi.email + managers_query = allocation.project.projectuser_set.filter( + role__name="Manager", status__name="Active", enable_notifications=True + ) + manager_emails = [ + manager.user.email + for manager in managers_query + if manager.user.email != pi_email + ] + + mail.send_email( + subject="Allocation Expiration Reminder", + body=EXPIRATION_REMINDER_TEMPLATE.format( + allocation=allocation, + ), + sender=EMAIL_SENDER, + receiver_list=[pi_email], + cc=manager_emails, + ) diff --git a/src/coldfront_plugin_cloud/tests/unit/test_allocation_alerts.py b/src/coldfront_plugin_cloud/tests/unit/test_allocation_alerts.py new file mode 100644 index 00000000..476bcf0f --- /dev/null +++ b/src/coldfront_plugin_cloud/tests/unit/test_allocation_alerts.py @@ -0,0 +1,47 @@ +from unittest.mock import patch +from datetime import datetime, timezone, timedelta + +from coldfront_plugin_cloud.tasks import remind_allocations_revoked +from coldfront_plugin_cloud.tests import base + + +class TestAllocationAlerts(base.TestBase): + @patch("coldfront_plugin_cloud.tasks.EMAIL_SENDER", "test@example.com") + def test_remind_allocations_expired(self): + """Test that remind_allocations_expired sends emails for allocations expiring soon.""" + # Setup mock datetime to control what "now" returns + # fake_now = datetime(2025, 11, 1, tzinfo=timezone.utc) + # mock_datetime.now.return_value = fake_now + + # Create test data + resource = self.new_openstack_resource( + name="TestResource", internal_name="TestResource" + ) + project = self.new_project() + + # Create allocation expiring in 30 days (should trigger reminder) + allocation = self.new_allocation( + project=project, + resource=resource, + quantity=1, + status="Expired", + ) + + manager = self.new_user() + self.new_project_user(manager, project, role="Manager") + + expiration_date = datetime.now(timezone.utc) + timedelta(days=30) + allocation.end_date = expiration_date + allocation.save() + + with patch("coldfront.core.utils.mail.send_email") as mock_send_email: + remind_allocations_revoked() + + # Assert mail.send_email was called once + mock_send_email.assert_called_once_with( + subject="Allocation Expiration Reminder", + sender="test@example.com", + receiver_list=[allocation.project.pi.email], + cc=[manager.email], + body="", + )