Skip to content

Commit

Permalink
feat: updates tasks usage of create_recipient to create_recipients
Browse files Browse the repository at this point in the history
  • Loading branch information
brobro10000 committed May 8, 2024
1 parent a69d8a1 commit 7346f99
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 70 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Unreleased
----------
* nothing unreleased

[4.18.0]
--------
* feat: updates tasks usage of create_recipient to create_recipients

[4.17.5]
--------
* fix: hard deleting expired group memberships
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "4.17.5"
__version__ = "4.18.0"
41 changes: 2 additions & 39 deletions enterprise/api_client/braze.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
logger = logging.getLogger(__name__)

ENTERPRISE_BRAZE_ALIAS_LABEL = 'Enterprise' # Do Not change this, this is consistent with other uses across edX repos.
# https://www.braze.com/docs/api/endpoints/user_data/post_user_identify/
MAX_NUM_IDENTIFY_USERS_ALIASES = 50


class BrazeAPIClient(BrazeClient):
Expand Down Expand Up @@ -41,45 +43,6 @@ def generate_mailto_link(self, emails):

return None

def create_recipient(
self,
user_email,
lms_user_id,
trigger_properties=None,
):
"""
Create a recipient object using the given user_email and lms_user_id.
Identifies the given email address with any existing Braze alias records
via the provided ``lms_user_id``.
"""

user_alias = {
'alias_label': ENTERPRISE_BRAZE_ALIAS_LABEL,
'alias_name': user_email,
}

# Identify the user alias in case it already exists. This is necessary so
# we don't accidentally create a duplicate Braze profile.
self.identify_users([{
'external_id': lms_user_id,
'user_alias': user_alias
}])

attributes = {
"user_alias": user_alias,
"email": user_email,
"is_enterprise_learner": True,
"_update_existing_only": False,
}

return {
'external_user_id': lms_user_id,
'attributes': attributes,
# If a profile does not already exist, Braze will create a new profile before sending a message.
'send_to_existing_only': False,
'trigger_properties': trigger_properties or {},
}

def create_recipient_no_external_id(self, user_email):
"""
Create a Braze recipient dict identified only by an alias based on their email.
Expand Down
85 changes: 64 additions & 21 deletions enterprise/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
from django.core import mail
from django.db import IntegrityError

from enterprise.api_client.braze import ENTERPRISE_BRAZE_ALIAS_LABEL, BrazeAPIClient
from enterprise.api_client.braze import ENTERPRISE_BRAZE_ALIAS_LABEL, MAX_NUM_IDENTIFY_USERS_ALIASES, BrazeAPIClient
from enterprise.api_client.enterprise_catalog import EnterpriseCatalogApiClient
from enterprise.constants import SSO_BRAZE_CAMPAIGN_ID
from enterprise.utils import get_enterprise_customer, send_email_notification_message
from enterprise.utils import batch_dict, get_enterprise_customer, send_email_notification_message

LOGGER = getLogger(__name__)

Expand Down Expand Up @@ -209,6 +209,58 @@ def send_sso_configured_email(
raise exc


def _recipients_for_identified_users(
user_id_by_email,
maximum_aliases_per_batch=MAX_NUM_IDENTIFY_USERS_ALIASES,
alias_label=ENTERPRISE_BRAZE_ALIAS_LABEL
):
"""
Helper function for create_recipients that takes a dictionary of user_email keys and
user_id values, batches them in groups of the maximum number of users alias based
on the braze documentation, and destructures the individual recipient values from
recipients_by_email and returns a list of recipients.
Arguments:
* user_id_by_email (dict): A dictionary of user_email key and user_id values
* maximum_aliases_per_batch (int): An integer denoting the max allowable aliases to identify
per create_recipients call to braze.
Default is MAX_NUM_IDENTIFY_USERS_ALIASES
* alias_label (string): A string denoting the alias label requried by braze.
Default is ENTERPRISE_BRAZE_ALIAS_LABEL
Return:
* recipients (list): A list of dictionary recipients
Example:
Input:
user_id_by_email = {
'test@gmail.com': 12345
}
maximum_aliases_per_batch: 50
alias_label: Titans
Output: [
{
'external_user_id: 12345,
'attributes: {
'user_alias': {
'external_id': 12345,
'alias_label': 'Titans'
},
},
},
]
"""
braze_client_instance = BrazeAPIClient()
recipients = []
for user_id_by_email_chunk in batch_dict(user_id_by_email, maximum_aliases_per_batch):
recipients_by_email = braze_client_instance.create_recipients(
alias_label,
user_id_by_email=user_id_by_email_chunk
)
recipients.extend(recipients_by_email.values())
return recipients


@shared_task
@set_code_owner_attribute
def send_group_membership_invitation_notification(
Expand Down Expand Up @@ -240,27 +292,23 @@ def send_group_membership_invitation_notification(

braze_trigger_properties['act_by_date'] = act_by_date.strftime('%B %d, %Y')
pecu_emails = []
ecus = []
user_id_by_email = {}
membership_records = enterprise_group_membership_model().objects.filter(uuid__in=membership_uuids)
for group_membership in membership_records:
if group_membership.pending_enterprise_customer_user is not None:
pecu_emails.append(group_membership.pending_enterprise_customer_user.user_email)
else:
ecus.append({
'user_email': group_membership.enterprise_customer_user.user_email,
'user_id': group_membership.enterprise_customer_user.user_id
})
user_id_by_email[
group_membership.enterprise_customer_user.user_email
] = group_membership.enterprise_customer_user.user_id
recipients = []
for pecu_email in pecu_emails:
recipients.append(braze_client_instance.create_recipient_no_external_id(pecu_email))
braze_client_instance.create_braze_alias(
[pecu_emails],
ENTERPRISE_BRAZE_ALIAS_LABEL,
)
for ecu in ecus:
recipients.append(braze_client_instance.create_recipient(
user_email=ecu['user_email'],
lms_user_id=ecu['user_id']))
recipients.extend(_recipients_for_identified_users(user_id_by_email))
try:
braze_client_instance.send_campaign_message(
settings.BRAZE_GROUPS_INVITATION_EMAIL_CAMPAIGN_ID,
Expand Down Expand Up @@ -298,16 +346,15 @@ def send_group_membership_removal_notification(enterprise_customer_uuid, members
'catalog_content_count'
] = enterprise_catalog_client.get_catalog_content_count(catalog_uuid)
pecu_emails = []
ecus = []
user_id_by_email = {}
membership_records = enterprise_group_membership_model().objects.filter(uuid__in=membership_uuids)
for group_membership in membership_records:
if group_membership.pending_enterprise_customer_user is not None:
pecu_emails.append(group_membership.pending_enterprise_customer_user.user_email)
else:
ecus.append({
'user_email': group_membership.enterprise_customer_user.user_email,
'user_id': group_membership.enterprise_customer_user.user_id
})
user_id_by_email[
group_membership.enterprise_customer_user.user_email
] = group_membership.enterprise_customer_user.user_id

recipients = []
for pecu_email in pecu_emails:
Expand All @@ -316,11 +363,7 @@ def send_group_membership_removal_notification(enterprise_customer_uuid, members
[pecu_emails],
ENTERPRISE_BRAZE_ALIAS_LABEL,
)
for ecu in ecus:
recipients.append(braze_client_instance.create_recipient(
user_email=ecu['user_email'],
lms_user_id=ecu['user_id']
))
recipients.extend(_recipients_for_identified_users(user_id_by_email))
try:
braze_client_instance.send_campaign_message(
settings.BRAZE_GROUPS_REMOVAL_EMAIL_CAMPAIGN_ID,
Expand Down
17 changes: 17 additions & 0 deletions enterprise/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import re
from collections import OrderedDict
from itertools import islice
from urllib.parse import parse_qs, quote, urlencode, urljoin, urlparse, urlsplit, urlunsplit
from uuid import UUID, uuid4

Expand Down Expand Up @@ -2297,6 +2298,22 @@ def batch(iterable, batch_size=1):
yield iterable[index:min(index + batch_size, iterable_len)]


def batch_dict(dict_data, chunk_size=1):
"""
Breaks up a dictionary into equal-sized chunks.
No fillers values are added for any 'remainder' chunks
Arguments:
dict (dict): A dictionary to chunk
chunk_size (int): the size of each chunk. Defaults to 1.
Returns:
generator: iterates through each chunk of a dictionary
"""
it = iter(dict_data.items())
for _ in range(0, len(dict_data), chunk_size):
yield dict(islice(it, chunk_size))


def get_best_mode_from_course_key(course_key):
"""
Helper method to retrieve a list of enrollments for a given course and select the one most applicable to enroll an
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ edx-api-doc-tools==1.8.0
# -r requirements/doc.txt
# -r requirements/test-master.txt
# -r requirements/test.txt
edx-braze-client==0.2.2
edx-braze-client==0.2.5
# via
# -r requirements/doc.txt
# -r requirements/test-master.txt
Expand Down
2 changes: 1 addition & 1 deletion requirements/doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ drf-yasg==1.21.5
# edx-api-doc-tools
edx-api-doc-tools==1.8.0
# via -r requirements/test-master.txt
edx-braze-client==0.2.2
edx-braze-client==0.2.5
# via -r requirements/test-master.txt
edx-django-utils==5.13.0
# via
Expand Down
2 changes: 1 addition & 1 deletion requirements/edx-platform-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ edx-auth-backends==4.3.0
# via
# -r requirements/edx/kernel.in
# openedx-blockstore
edx-braze-client==0.2.2
edx-braze-client==0.2.5
# via
# -r requirements/edx/bundled.in
# edx-enterprise
Expand Down
2 changes: 1 addition & 1 deletion requirements/test-master.txt
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ edx-api-doc-tools==1.8.0
# via
# -c requirements/edx-platform-constraints.txt
# -r requirements/test-master.in
edx-braze-client==0.2.2
edx-braze-client==0.2.5
# via
# -c requirements/edx-platform-constraints.txt
# -r requirements/base.in
Expand Down
2 changes: 1 addition & 1 deletion requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ drf-yasg==1.21.5
# edx-api-doc-tools
edx-api-doc-tools==1.8.0
# via -r requirements/test-master.txt
edx-braze-client==0.2.2
edx-braze-client==0.2.5
# via -r requirements/test-master.txt
edx-django-utils==5.13.0
# via
Expand Down
18 changes: 18 additions & 0 deletions test_utils/fake_user_id_by_emails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Utility functions for tests
"""
import math
import random
import string


def generate_emails_and_ids(num_emails):
"""
Generates random emails with random uuids used primarily to test length constraints
"""
emails_and_ids = {
''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) +
'@gmail.com': math.floor(random.random() * 1000)
for _ in range(num_emails)
}
return emails_and_ids
50 changes: 46 additions & 4 deletions tests/test_enterprise/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,29 @@ def test_send_group_membership_invitation_notification(self, mock_braze_api_clie
activated_at=datetime.now()
)
admin_email = 'edx@example.org'
mock_recipients = [self.pending_enterprise_customer_user.user_email,
mock_braze_api_client().create_recipient.return_value]
mock_braze_api_client().create_recipients.return_value = {
self.user.email: {
"external_user_id": self.user.id,
"attributes": {
"user_alias": {
"external_id": self.user.id,
"user_alias": self.user.email,
},
}
}
}
mock_recipients = [
self.pending_enterprise_customer_user.user_email,
{
"external_user_id": self.user.id,
"attributes": {
"user_alias": {
"external_id": self.user.id,
"user_alias": self.user.email,
},
}
}
]
mock_catalog_content_count = 5
mock_admin_mailto = f'mailto:{admin_email}'
mock_braze_api_client().generate_mailto_link.return_value = mock_admin_mailto
Expand Down Expand Up @@ -261,8 +282,29 @@ def test_send_group_membership_removal_notification(self, mock_braze_api_client,
activated_at=datetime.now()
)
admin_email = 'edx@example.org'
mock_recipients = [self.pending_enterprise_customer_user.user_email,
mock_braze_api_client().create_recipient.return_value]
mock_braze_api_client().create_recipients.return_value = {
self.user.email: {
"external_user_id": self.user.id,
"attributes": {
"user_alias": {
"external_id": self.user.id,
"user_alias": self.user.email,
},
}
}
}
mock_recipients = [
self.pending_enterprise_customer_user.user_email,
{
"external_user_id": self.user.id,
"attributes": {
"user_alias": {
"external_id": self.user.id,
"user_alias": self.user.email,
},
}
}
]
mock_admin_mailto = f'mailto:{admin_email}'
mock_braze_api_client().generate_mailto_link.return_value = mock_admin_mailto
mock_braze_api_client().create_recipient_no_external_id.return_value = (
Expand Down
Loading

0 comments on commit 7346f99

Please sign in to comment.