diff --git a/src/coldfront_plugin_cloud/management/commands/fetch_daily_billable_usage.py b/src/coldfront_plugin_cloud/management/commands/fetch_daily_billable_usage.py index 93b447d0..3dc329b3 100644 --- a/src/coldfront_plugin_cloud/management/commands/fetch_daily_billable_usage.py +++ b/src/coldfront_plugin_cloud/management/commands/fetch_daily_billable_usage.py @@ -48,10 +48,9 @@ CENTER_BASE_URL = import_from_settings("CENTER_BASE_URL") EMAIL_SENDER = import_from_settings("EMAIL_SENDER") -EMAIL_ENABLED = import_from_settings("EMAIL_ENABLED") EMAIL_TEMPLATE = """Dear New England Research Cloud user, -Your {resource.name} {resource.type} Allocation in project {allocation.project.title} has reached your preset Alert value. +Your {resource.name} {resource.resource_type} Allocation in project {allocation.project.title} has reached your preset Alert value. - As of midnight last night, your Allocation reached or exceeded your preset Alert value of {alert_value}. - To view your Allocation information visit {url}/allocation/{allocation.id} @@ -259,11 +258,11 @@ def handle_alerting( f"{allocation.id} of {allocation.project.title} exceeded" f"alerting value of {allocation_alerting_value}." ) - if not already_alerted and EMAIL_ENABLED: + if not already_alerted: try: cls.send_alert_email( allocation, - allocation.resources.first().name, + allocation.get_parent_resource, allocation_alerting_value, ) logger.info( @@ -276,15 +275,28 @@ def handle_alerting( ) @staticmethod - def send_alert_email(allocation: Allocation, resource: Resource, alert_value): - mail.send_mail( + def get_managers(allocation: Allocation): + """Returns list of managers with enabled notifications.""" + managers_query = allocation.project.projectuser_set.filter( + role__name="Manager", status__name="Active", enable_notifications=True + ) + return [manager.user.email for manager in managers_query] + + @classmethod + def send_alert_email(cls, allocation: Allocation, resource: Resource, alert_value): + mail.send_email( subject="Allocation Usage Alert", - message=EMAIL_TEMPLATE.format( + body=EMAIL_TEMPLATE.format( allocation=allocation, resource=resource, alert_value=alert_value, url=CENTER_BASE_URL, ), - from_email=EMAIL_SENDER, - recipient_list=[allocation.project.pi.email], + sender=EMAIL_SENDER, + receiver_list=[allocation.project.pi.email], + cc=[ + x + for x in cls.get_managers(allocation) + if x != allocation.project.pi.email + ], ) diff --git a/src/coldfront_plugin_cloud/tests/unit/test_fetch_daily_billable_usage.py b/src/coldfront_plugin_cloud/tests/unit/test_fetch_daily_billable_usage.py index 43f82e4d..9b43217d 100644 --- a/src/coldfront_plugin_cloud/tests/unit/test_fetch_daily_billable_usage.py +++ b/src/coldfront_plugin_cloud/tests/unit/test_fetch_daily_billable_usage.py @@ -1,4 +1,5 @@ import io +from unittest import mock from unittest.mock import Mock, patch @@ -21,6 +22,18 @@ test-allocation-2,OpenStack CPU,0.25 """ +OUTPUT_EMAIL_TEMPLATE = """Dear New England Research Cloud user, + +Your FakeProd OpenStack Allocation in project FakeProject has reached your preset Alert value. + +- As of midnight last night, your Allocation reached or exceeded your preset Alert value of 100. +- To view your Allocation information visit http://localhost/allocation/{allocation_id} + +Thank you, +New England Research Cloud (NERC) +https://nerc.mghpcc.org/ +""" + class TestFetchDailyBillableUsage(base.TestBase): def test_get_daily_location_for_prefix(self): @@ -107,10 +120,6 @@ def test_get_allocations_for_daily_billing(self): self.assertNotIn(prod_allocation_3, returned_allocation_ids) self.assertNotIn(dev_allocation_1, returned_allocation_ids) - @patch( - "coldfront_plugin_cloud.management.commands.fetch_daily_billable_usage.EMAIL_ENABLED", - True, - ) @patch( "coldfront_plugin_cloud.management.commands.fetch_daily_billable_usage.RESOURCES_DAILY_ENABLED", ["FakeProd"], @@ -179,7 +188,7 @@ def test_call_command(self, mock_get_allocation_usage): "coldfront_plugin_cloud.management.commands.fetch_daily_billable_usage.Command.send_alert_email" ) as mock: call_command("fetch_daily_billable_usage", date="2025-11-16") - mock.assert_called_once_with(allocation_1, fakeprod.name, 200) + mock.assert_called_once_with(allocation_1, fakeprod, 200) self.assertEqual( allocation_1.get_attribute(attributes.ALLOCATION_CUMULATIVE_CHARGES), "2025-11-16: 225.00 USD", @@ -192,3 +201,40 @@ def test_call_command(self, mock_get_allocation_usage): allocation_1.get_attribute(attributes.ALLOCATION_CUMULATIVE_CHARGES), "2025-11-16: 225.00 USD", ) + + @patch( + "coldfront_plugin_cloud.management.commands.fetch_daily_billable_usage.CENTER_BASE_URL", + "http://localhost", + ) + @patch( + "coldfront_plugin_cloud.management.commands.fetch_daily_billable_usage.EMAIL_SENDER", + "test@example.com", + ) + def test_send_alert_email(self): + fakeprod = self.new_openstack_resource( + name="FakeProd", internal_name="FakeProd" + ) + prod_project = self.new_project(title="FakeProject") + allocation_1 = self.new_allocation( + project=prod_project, resource=fakeprod, quantity=1, status="Active" + ) + + manager = self.new_user() + self.new_project_user(manager, prod_project, role="Manager") + + normal_user = self.new_user() + self.new_project_user(normal_user, prod_project, role="User") + + with mock.patch("coldfront.core.utils.mail.send_email") as mock_send_email: + Command.send_alert_email( + allocation=allocation_1, resource=fakeprod, alert_value=100 + ) + mock_send_email.assert_called_once_with( + subject="Allocation Usage Alert", + body=OUTPUT_EMAIL_TEMPLATE.format( + allocation_id=allocation_1.id, + ), + sender="test@example.com", + receiver_list=[allocation_1.project.pi.email], + cc=[manager.email], + )