From ebb26661ab4cd695ed975a57b70bc679817297fa Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Tue, 10 Sep 2024 21:22:58 +0000 Subject: [PATCH] feat: default stale start_dates to today in braze assignment task --- .../apps/content_assignments/constants.py | 2 + .../apps/content_assignments/tasks.py | 13 ++++- .../content_assignments/tests/test_tasks.py | 13 +++-- .../apps/content_assignments/utils.py | 55 +++++++++++++++++++ 4 files changed, 76 insertions(+), 7 deletions(-) diff --git a/enterprise_access/apps/content_assignments/constants.py b/enterprise_access/apps/content_assignments/constants.py index 9cd09646..162f890d 100644 --- a/enterprise_access/apps/content_assignments/constants.py +++ b/enterprise_access/apps/content_assignments/constants.py @@ -127,6 +127,8 @@ class AssignmentAutomaticExpiredReason: NUM_DAYS_BEFORE_AUTO_EXPIRATION = 90 +START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS = 14 + RETIRED_EMAIL_ADDRESS_FORMAT = 'retired_user{}@retired.invalid' BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ" diff --git a/enterprise_access/apps/content_assignments/tasks.py b/enterprise_access/apps/content_assignments/tasks.py index 78a8c004..f3d8956c 100644 --- a/enterprise_access/apps/content_assignments/tasks.py +++ b/enterprise_access/apps/content_assignments/tasks.py @@ -1,7 +1,6 @@ """ Tasks for content_assignments app. """ - import logging from braze.exceptions import BrazeBadRequestError @@ -26,6 +25,7 @@ ) from .constants import BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT, LearnerContentAssignmentStateChoices +from .utils import get_self_paced_normalized_start_date logger = logging.getLogger(__name__) @@ -211,8 +211,17 @@ def get_enrollment_deadline(self): return get_human_readable_date(self._enrollment_deadline_raw()) def get_start_date(self): + """ + Checks if the start_date is before today's date offset by the + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS value and returns today's date + If not, pass through the formatted start date. + """ + start_date = self.normalized_metadata.get('start_date') + end_date = self.normalized_metadata.get('end_date') + course_metadata = self.course_metadata + return get_human_readable_date( - self.normalized_metadata.get('start_date') + get_self_paced_normalized_start_date(start_date, end_date, course_metadata) ) def get_action_required_by_timestamp(self): diff --git a/enterprise_access/apps/content_assignments/tests/test_tasks.py b/enterprise_access/apps/content_assignments/tests/test_tasks.py index 8e037799..e39cdf47 100644 --- a/enterprise_access/apps/content_assignments/tests/test_tasks.py +++ b/enterprise_access/apps/content_assignments/tests/test_tasks.py @@ -1,6 +1,7 @@ """ Tests for Enterprise Access content_assignments tasks. """ +import datetime from unittest import mock from uuid import uuid4 @@ -20,7 +21,7 @@ AssignmentActions, LearnerContentAssignmentStateChoices ) -from enterprise_access.apps.content_assignments.content_metadata_api import format_datetime_obj +from enterprise_access.apps.content_assignments.content_metadata_api import format_datetime_obj, get_human_readable_date from enterprise_access.apps.content_assignments.tasks import ( BrazeCampaignSender, create_pending_enterprise_learner_for_assignment_task, @@ -240,6 +241,9 @@ def setUpTestData(cls): ], 'card_image_url': 'https://itsanimage.com', } + cls.mock_formatted_todays_date = get_human_readable_date(datetime.datetime.now().strftime( + BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT + )) def setUp(self): super().setUp() @@ -382,6 +386,7 @@ def test_send_reminder_email_for_pending_assignment( [self.assignment.learner_email], ENTERPRISE_BRAZE_ALIAS_LABEL, ) + mock_braze_client.send_campaign_message.assert_called_once_with( expected_campaign_identifier, recipients=[expected_recipient], @@ -390,7 +395,7 @@ def test_send_reminder_email_for_pending_assignment( 'organization': self.enterprise_customer_name, 'course_title': self.assignment.content_title, 'enrollment_deadline': 'Jan 01, 2021', - 'start_date': 'Jan 01, 2020', + 'start_date': self.mock_formatted_todays_date, 'course_partner': 'Smart Folks, Good People, and Fast Learners', 'course_card_image': 'https://itsanimage.com', 'learner_portal_link': 'http://enterprise-learner-portal.example.com/test-slug', @@ -420,7 +425,6 @@ def test_send_email_for_new_assignment( 'count': 1, 'results': [self.mock_content_metadata] } - # Set the subsidy expiration time to tomorrow mock_subsidy = { 'uuid': self.policy.subsidy_uuid, @@ -439,7 +443,6 @@ def test_send_email_for_new_assignment( mock_lms_client.return_value.get_enterprise_customer_data.assert_called_with( self.assignment_configuration.enterprise_customer_uuid ) - mock_braze_client.return_value.send_campaign_message.assert_any_call( 'test-assignment-notification-campaign', recipients=[mock_recipient], @@ -448,7 +451,7 @@ def test_send_email_for_new_assignment( 'organization': self.enterprise_customer_name, 'course_title': self.assignment.content_title, 'enrollment_deadline': 'Jan 01, 2021', - 'start_date': 'Jan 01, 2020', + 'start_date': self.mock_formatted_todays_date, 'course_partner': 'Smart Folks and Good People', 'course_card_image': self.mock_content_metadata['card_image_url'], 'learner_portal_link': '{}/{}'.format( diff --git a/enterprise_access/apps/content_assignments/utils.py b/enterprise_access/apps/content_assignments/utils.py index e69de29b..4ade5c5e 100644 --- a/enterprise_access/apps/content_assignments/utils.py +++ b/enterprise_access/apps/content_assignments/utils.py @@ -0,0 +1,55 @@ +""" +Utils for content_assignments +""" +from datetime import datetime, timedelta + +from dateutil import parser +from pytz import UTC + +from enterprise_access.apps.content_assignments.constants import ( + BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT, + START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS +) + + +def is_within_minimum_start_date_threshold(today, start_date): + """ + Checks if today's date were set to a certain number of days in the past, + offset_date_from_today, is the start_date before offset_date_from_today. + """ + start_date_datetime = parser.parse(start_date) + offset_date_from_today = today - timedelta(days=START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS) + return start_date_datetime < offset_date_from_today.replace(tzinfo=UTC) + + +def has_time_to_complete(today, end_date, weeks_to_complete): + """ + Checks if today's date were set to a certain number of weeks_to_complete in the future, + offset_now_by_weeks_to_complete, is offset_now_by_weeks_to_complete date before the end_date + """ + end_date_datetime = parser.parse(end_date) + offset_now_by_weeks_to_complete = today + timedelta(weeks=weeks_to_complete) + return offset_now_by_weeks_to_complete.replace(tzinfo=UTC) <= end_date_datetime + + +def get_self_paced_normalized_start_date(start_date, end_date, course_metadata): + """ + Normalizes courses start_date far in the past based on a heuristic for the purpose of displaying a + reasonable start_date in content assignment related emails. + + Heuristic: + For self-paced courses with a weeks_to_complete field too close to the end date to complete the course + or a start_date that is before today offset by the START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS should + default to today's date. + Otherwise, return the current start_date + """ + today = datetime.now() + pacing_type = course_metadata.get('pacing_type') or None + weeks_to_complete = course_metadata.get('weeks_to_complete') or None + if not (start_date and end_date and pacing_type and weeks_to_complete): + return today.strftime(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT) + if pacing_type == "self_paced": + if has_time_to_complete(today, end_date, weeks_to_complete) or \ + is_within_minimum_start_date_threshold(today, start_date): + return today.strftime(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT) + return start_date