Skip to content

Commit

Permalink
feat: Nudge API
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 committed Jan 29, 2024
1 parent 60b62a1 commit e1d2c2d
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,35 @@ class LearnerContentAssignmentActionRequestSerializer(serializers.Serializer):
)


class LearnerContentAssignmentNudgeRequestSerializer(serializers.Serializer):
"""
Request serializer to validate nudge endpoint query params.
For view: LearnerContentAssignmentAdminViewSet.nudge
"""
assignment_uuids = serializers.ListField(
child=serializers.UUIDField(),
allow_empty=False
)
days_before_course_start_date = serializers.IntegerField(
min_value=1
)


class LearnerContentAssignmentNudgeResponseSerializer(serializers.Serializer):
"""
Response serializer for nudge endpoint.
For view: LearnerContentAssignmentAdminViewSet.nudge
"""
nudged_assignment_uuids = serializers.ListField(
child=serializers.UUIDField(),
)
unnudged_assignment_uuids = serializers.ListField(
child=serializers.UUIDField(),
)


class ContentMetadataForAssignmentSerializer(serializers.Serializer):
"""
Serializer to help return additional content metadata for assignments. These fields should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@

from enterprise_access.apps.api import filters, serializers, utils
from enterprise_access.apps.api.serializers.content_assignments.assignment import (
LearnerContentAssignmentActionRequestSerializer
LearnerContentAssignmentActionRequestSerializer,
LearnerContentAssignmentNudgeRequestSerializer,
LearnerContentAssignmentNudgeResponseSerializer
)
from enterprise_access.apps.api.v1.views.utils import PaginationWithPageCount
from enterprise_access.apps.content_assignments import api as assignments_api
Expand Down Expand Up @@ -347,3 +349,39 @@ def remind_all(self, request, *args, **kwargs):
return Response(status=status.HTTP_202_ACCEPTED)
except Exception: # pylint: disable=broad-except
return Response(status=status.HTTP_422_UNPROCESSABLE_ENTITY)

@extend_schema(
tags=[CONTENT_ASSIGNMENT_ADMIN_CRUD_API_TAG],
summary='Nudge assignments by UUID.',
request=LearnerContentAssignmentNudgeRequestSerializer,
parameters=None,
responses={
status.HTTP_200_OK: LearnerContentAssignmentNudgeResponseSerializer,
status.HTTP_422_UNPROCESSABLE_ENTITY: None,
}
)
@permission_required(CONTENT_ASSIGNMENT_ADMIN_WRITE_PERMISSION, fn=assignment_admin_permission_fn)
@action(detail=False, methods=['post'])
def nudge(self, request, *args, **kwargs):
"""
Send nudges to a list of learners with associated ``LearnerContentAssignment``
record by list of uuids.
```
Raises:
400 If ``assignment_uuids`` list length is 0 or the value for ``days_before_course_start_date`` is less than 1
422 If the nudge_assignments call fails for any other reason
```
"""
serializer = LearnerContentAssignmentNudgeRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
assignments = self.get_queryset().filter(
assignment_configuration__uuid=self.requested_assignment_configuration_uuid,
uuid__in=serializer.data['assignment_uuids'],
)
days_before_course_start_date = serializer.data['days_before_course_start_date']
try:
response = assignments_api.nudge_assignments(assignments, days_before_course_start_date)
return Response(data=response, status=status.HTTP_200_OK)
except Exception: # pylint: disable=broad-except
return Response(status=status.HTTP_422_UNPROCESSABLE_ENTITY)
101 changes: 100 additions & 1 deletion enterprise_access/apps/content_assignments/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@
from django.utils.timezone import now
from pytz import UTC

from enterprise_access.apps.content_assignments.tasks import send_reminder_email_for_pending_assignment
from enterprise_access.apps.content_assignments.content_metadata_api import (
get_content_metadata_for_assignments,
is_date_n_days_from_now,
parse_datetime_string
)
from enterprise_access.apps.content_assignments.tasks import (
send_exec_ed_enrollment_warmer,
send_reminder_email_for_pending_assignment
)
from enterprise_access.apps.core.models import User
from enterprise_access.apps.subsidy_access_policy.content_metadata_api import get_and_cache_content_metadata

Expand Down Expand Up @@ -570,6 +578,97 @@ def remind_assignments(assignments: Iterable[LearnerContentAssignment]) -> dict:
}


def nudge_assignments(assignments, days_before_course_start_date):
"""
Nudge assignments.
This is a no-op for assignments in the following state: [allocated, errored, canceled, expired]. We only allow
assignments which are in the accepted state.
Args:
assignment: An assignment to nudge
days_before_course_start_date: Number of days prior to start date to nudge individual assignment
content_metadata: Content metadata of the assigned course to be nudged
"""
nudged_assignment_uuids = []
unnudged_assignment_uuids = []

for assignment in assignments:
if assignment.state == LearnerContentAssignmentStateChoices.ACCEPTED:
assignment_configuration = AssignmentConfiguration.objects.filter(uuid=assignment.assignment_configuration)
subsidy_access_policy = assignment_configuration.subsidy_access_policy
enterprise_catalog_uuid = subsidy_access_policy.catalog_uuid

message = (
'[API_BRAZE_EMAIL_CAMPAIGN_NUDGING_1] '
'Assignment Configuration. UUID: [%s], '
'Policy: [%s], Catalog: [%s], Enterprise: [%s]',
)
logger.info(
message,
assignment_configuration.uuid,
subsidy_access_policy.uuid,
enterprise_catalog_uuid,
assignment_configuration.enterprise_customer_uuid,
)

content_metadata_for_assignments = get_content_metadata_for_assignments(
enterprise_catalog_uuid,
[assignment],
)
content_metadata = content_metadata_for_assignments.get(assignment.content_key, {})
start_date = content_metadata.get('normalized_metadata', {}).get('start_date')
course_type = content_metadata.get('course_type')

is_executive_education_course_type = course_type == 'executive-education-2u'

# Determine if the date from today + days_before_course_state_date is
# equal to the date of the start date
# If they are equal, then send the nudge email, otherwise continue
datetime_start_date = parse_datetime_string(start_date, set_to_utc=True)
can_send_nudge_notification_in_advance = is_date_n_days_from_now(
target_datetime=datetime_start_date,
num_days=days_before_course_start_date
)

if is_executive_education_course_type and can_send_nudge_notification_in_advance:
message = (
'[API_BRAZE_EMAIL_CAMPAIGN_NUDGING_2] ',
'assignment_configuration_uuid: [%s], start_date: [%s], datetime_start_date: [%s], '
'days_before_course_start_date: [%s], can_send_nudge_notification_in_advance: [%s], '
'course_type: [%s]'
)
logger.info(
message,
assignment.assignment_configuration,
start_date,
datetime_start_date,
days_before_course_start_date,
can_send_nudge_notification_in_advance,
course_type,
)
send_exec_ed_enrollment_warmer.delay(assignment.uuid, days_before_course_start_date)
nudged_assignment_uuids.append(assignment.uuid)
else:
unnudged_assignment_uuids.append(assignment.uuid)
else:
message = (
'[API_BRAZE_EMAIL_CAMPAIGN_NUDGING_ERROR] '
'assignment: [%s], '
'days_before_course_start_date: [%s]'
)
logger.info(
message,
assignment, days_before_course_start_date
)
unnudged_assignment_uuids.append(assignment.uuid)
return {
'nudged_assignment_uuids': nudged_assignment_uuids,
'unnudged_assignment_uuids': unnudged_assignment_uuids,
}


def expire_assignment(assignment, content_metadata, modify_assignment=True):
"""
If applicable, retires the given assignment, returning an expiration reason.
Expand Down

0 comments on commit e1d2c2d

Please sign in to comment.