Skip to content

Commit

Permalink
feat: default stale start_dates to today in braze assignment task (#560)
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 authored Sep 23, 2024
1 parent b79810d commit 3613a5e
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 7 deletions.
2 changes: 2 additions & 0 deletions enterprise_access/apps/content_assignments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
12 changes: 10 additions & 2 deletions enterprise_access/apps/content_assignments/tasks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""
Tasks for content_assignments app.
"""

import logging

from braze.exceptions import BrazeBadRequestError
Expand All @@ -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__)

Expand Down Expand Up @@ -211,8 +211,16 @@ 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 matches the criteria set by `get_self_paced_normalized_start_date`
for old start_dates, if so, return today's date, otherwise, return the 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):
Expand Down
13 changes: 8 additions & 5 deletions enterprise_access/apps/content_assignments/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for Enterprise Access content_assignments tasks.
"""
import datetime
from unittest import mock
from uuid import uuid4

Expand All @@ -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,
Expand Down Expand Up @@ -251,6 +252,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()
Expand Down Expand Up @@ -432,6 +436,7 @@ def test_send_reminder_email_for_pending_assignment(
[assignment.learner_email],
ENTERPRISE_BRAZE_ALIAS_LABEL,
)

mock_braze_client.send_campaign_message.assert_called_once_with(
expected_campaign_identifier,
recipients=[expected_recipient],
Expand All @@ -440,7 +445,7 @@ def test_send_reminder_email_for_pending_assignment(
'organization': self.enterprise_customer_name,
'course_title': 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',
Expand Down Expand Up @@ -476,7 +481,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,
Expand All @@ -497,7 +501,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],
Expand All @@ -506,7 +509,7 @@ def test_send_email_for_new_assignment(
'organization': self.enterprise_customer_name,
'course_title': 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(
Expand Down
209 changes: 209 additions & 0 deletions enterprise_access/apps/content_assignments/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
Tests for Enterprise Access content_assignments utils.
"""
from datetime import datetime, timedelta

import ddt
from django.test import TestCase
from pytz import UTC

from enterprise_access.apps.content_assignments.constants import BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT
from enterprise_access.apps.content_assignments.utils import (
get_self_paced_normalized_start_date,
has_time_to_complete,
is_within_minimum_start_date_threshold
)


def _curr_date(date_format=None):
curr_date = datetime.now()
if not date_format:
return curr_date
return curr_date.strftime(date_format)


def _days_from_now(days_from_now=0, date_format=None):
date = datetime.now().replace(tzinfo=UTC) + timedelta(days=days_from_now)
if not date_format:
return date
return date.strftime(date_format)


@ddt.ddt
class UtilsTests(TestCase):
"""
Tests related to utility functions for content assignments
"""

@ddt.data(
# Start after is before the curr_date - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"expected_output": False
},
# Start after is before the curr_date - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(-5, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"expected_output": False
},
# Start after is before the curr_date - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(15, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"expected_output": False
},
# Start date is before the curr_date - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(-15, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"expected_output": True
},
# Start after is before the curr_date - START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _curr_date('%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"expected_output": False
}
)
@ddt.unpack
def test_is_within_minimum_start_date_threshold(self, start_date, curr_date, expected_output):
assert is_within_minimum_start_date_threshold(curr_date, start_date) == expected_output

@ddt.data(
# endDate is the exact day as weeks to complete offset
{
"end_date": _days_from_now(49, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"weeks_to_complete": 7,
"expected_output": True
},
# weeks to complete is within endDate
{
"end_date": _days_from_now(49, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"weeks_to_complete": 4,
"expected_output": True
},
# weeks to complete is beyond end date
{
"end_date": _days_from_now(49, '%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"weeks_to_complete": 8,
"expected_output": False
},
# end date is current date
{
"end_date": _curr_date('%Y-%m-%dT%H:%M:%SZ'),
"curr_date": _curr_date(),
"weeks_to_complete": 1,
"expected_output": False
},
)
@ddt.unpack
def test_has_time_to_complete(self, end_date, curr_date, weeks_to_complete, expected_output):
assert has_time_to_complete(curr_date, end_date, weeks_to_complete) == expected_output

@ddt.data(
{
"start_date": None,
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": 8,
},
},
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": None,
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": 8,
},
},
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": None,
"weeks_to_complete": 8,
},
},
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": None,
},
},
{
"start_date": None,
"end_date": None,
"course_metadata": {
"pacing_type": None,
"weeks_to_complete": None,
},
},
)
@ddt.unpack
def test_get_self_paced_normalized_start_date_empty_data(self, start_date, end_date, course_metadata):
assert get_self_paced_normalized_start_date(start_date, end_date, course_metadata) == \
_curr_date(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT)

@ddt.data(
# self-paced, has time to complete
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(28, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": 3,
},
},
# self-paced, does not have time to complete, but start date older than
# START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(-15, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": 300,
},
},
# self-paced, does not have time to complete, start date within
# START_DATE_DEFAULT_TO_TODAY_THRESHOLD_DAYS
{
"start_date": _days_from_now(-5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "self_paced",
"weeks_to_complete": 300,
},
},
# instructor paced
{
"start_date": _days_from_now(5, '%Y-%m-%dT%H:%M:%SZ'),
"end_date": _days_from_now(10, '%Y-%m-%dT%H:%M:%SZ'),
"course_metadata": {
"pacing_type": "instructor_paced",
"weeks_to_complete": 8,
},
},
)
@ddt.unpack
def test_get_self_paced_normalized_start_date_self_paced(self, start_date, end_date, course_metadata):
pacing_type = course_metadata.get('pacing_type')
weeks_to_complete = course_metadata.get('weeks_to_complete')

can_complete_in_time = has_time_to_complete(_curr_date(), end_date, weeks_to_complete)
within_start_date_threshold = is_within_minimum_start_date_threshold(_curr_date(), start_date)

if pacing_type == 'self_paced' and (can_complete_in_time or within_start_date_threshold):
assert get_self_paced_normalized_start_date(start_date, end_date, course_metadata) == \
_curr_date(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT)
else:
assert get_self_paced_normalized_start_date(start_date, end_date, course_metadata) == \
start_date
59 changes: 59 additions & 0 deletions enterprise_access/apps/content_assignments/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
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(curr_date, 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 = curr_date - 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(curr_date, 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 = curr_date + timedelta(weeks=weeks_to_complete)
print(offset_now_by_weeks_to_complete, end_date_datetime)
return offset_now_by_weeks_to_complete.replace(tzinfo=UTC).strftime("%Y-%m-%d") <= \
end_date_datetime.strftime("%Y-%m-%d")


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
"""
curr_date = 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 curr_date.strftime(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT)
print(has_time_to_complete(curr_date, end_date, weeks_to_complete),
is_within_minimum_start_date_threshold(curr_date, start_date))
if pacing_type == "self_paced":
if has_time_to_complete(curr_date, end_date, weeks_to_complete) or \
is_within_minimum_start_date_threshold(curr_date, start_date):
return curr_date.strftime(BRAZE_ACTION_REQUIRED_BY_TIMESTAMP_FORMAT)
return start_date

0 comments on commit 3613a5e

Please sign in to comment.