-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add a command to fetch unsubscribed emails from Braze
- Loading branch information
1 parent
74e3bb9
commit 4dca42a
Showing
5 changed files
with
323 additions
and
0 deletions.
There are no files selected for viewing
123 changes: 123 additions & 0 deletions
123
common/djangoapps/student/management/commands/retrieve_unsubscribed_emails.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
"""Management command to retrieve unsubscribed emails from Braze.""" | ||
|
||
import logging | ||
import tempfile | ||
from datetime import datetime, timedelta | ||
|
||
from django.conf import settings | ||
from django.core.mail.message import EmailMultiAlternatives | ||
from django.core.management.base import BaseCommand, CommandError | ||
from django.template.loader import get_template | ||
|
||
from lms.djangoapps.utils import get_braze_client | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
UNSUBSCRIBED_EMAILS_MAX_LIMIT = 500 | ||
|
||
|
||
class Command(BaseCommand): | ||
""" | ||
Management command to retrieve unsubscribed emails from Braze. | ||
""" | ||
|
||
help = """ | ||
Retrieve unsubscribed emails from Braze API based on specified parameters. | ||
Usage: | ||
python manage.py retrieve_unsubscribed_emails [--start-date START_DATE] [--end-date END_DATE] | ||
Options: | ||
--start-date START_DATE Start date (optional) | ||
--end-date END_DATE End date (optional) | ||
Example: | ||
$ ... retrieve_unsubscribed_emails --start-date 2022-01-01 --end-date 2023-01-01 | ||
""" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('--start_date', dest='start_date', help='Start date') | ||
parser.add_argument('--end_date', dest='end_date', help='End date') | ||
|
||
def _write_csv(self, csv, data): | ||
"""Write a test CSV file with the data provided""" | ||
headers = list(data[0].keys()) | ||
csv.write(','.join(headers).encode('utf-8') + b"\n") | ||
|
||
for row in data: | ||
values = [str(row[key]) for key in headers] | ||
csv.write(','.join(values).encode('utf-8') + b"\n") | ||
|
||
csv.seek(0) | ||
return csv | ||
|
||
def handle(self, *args, **options): | ||
emails = [] | ||
start_date = options.get('start_date') | ||
end_date = options.get('end_date') | ||
|
||
if not start_date and not end_date: | ||
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d') | ||
end_date = datetime.now().strftime('%Y-%m-%d') | ||
|
||
try: | ||
braze_client = get_braze_client() | ||
if braze_client: | ||
emails = braze_client.retrieve_unsubscribed_emails( | ||
start_date=start_date, | ||
end_date=end_date, | ||
) | ||
self.stdout.write(self.style.SUCCESS('Unsubscribed emails retrieved successfully')) | ||
except Exception as exc: | ||
logger.exception(f'Unable to retrieve unsubscribed emails from Braze due to exception: {exc}') | ||
raise CommandError( | ||
f'Unable to retrieve unsubscribed emails from Braze due to exception: {exc}' | ||
) from exc | ||
|
||
if emails: | ||
try: | ||
with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix='.csv') as csv_file: | ||
csv_file = self._write_csv(csv_file, emails) | ||
csv_file_path = csv_file.name | ||
except Exception as exc: | ||
logger.exception(f'Error while writing data in CSV file due to exception: {exc}') | ||
raise CommandError( | ||
f'Error while writing data in CSV file due to exception: {exc}' | ||
) from exc | ||
|
||
txt_template = 'unsubscribed_emails/email/email_body.txt' | ||
html_template = 'unsubscribed_emails/email/email_body.html' | ||
|
||
context = { | ||
'start_date': start_date, | ||
'end_date': end_date, | ||
} | ||
|
||
template = get_template(txt_template) | ||
plain_content = template.render(context) | ||
template = get_template(html_template) | ||
html_content = template.render(context) | ||
|
||
subject = f'Unsubscribed Emails from {start_date} to {end_date}' | ||
|
||
email_msg = EmailMultiAlternatives( | ||
subject, | ||
plain_content, | ||
settings.UNSUBSCRIBED_EMAILS_FROM_EMAIL, | ||
settings.UNSUBSCRIBED_EMAILS_RECIPIENT_EMAIL | ||
) | ||
email_msg.attach_alternative(html_content, 'text/html') | ||
|
||
with open(csv_file_path, 'rb') as file: | ||
email_msg.attach(filename='unsubscribed_emails.csv', content=file.read(), mimetype='text/csv') | ||
|
||
try: | ||
email_msg.send() | ||
logger.info('Unsubscribed emails data sent to your email.') | ||
except Exception as exc: | ||
logger.exception(f'Failure to send unsubscribed emails data to your email due to exception: {exc}') | ||
raise CommandError( | ||
f'Failure to send unsubscribed emails data to your email due to exception: {exc}' | ||
) from exc | ||
|
||
logger.info('No unsubscribed emails found within the specified date range.') |
177 changes: 177 additions & 0 deletions
177
common/djangoapps/student/management/tests/test_retrieve_unsubscribed_emails.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
"""Tests for retrieve unsubscribed emails management command""" | ||
|
||
import os | ||
from io import StringIO | ||
from tempfile import NamedTemporaryFile | ||
from unittest.mock import patch, MagicMock | ||
|
||
import six | ||
from django.core.management import call_command | ||
from django.core.management.base import CommandError | ||
from django.test import TestCase, override_settings | ||
|
||
|
||
class RetrieveUnsubscribedEmailsTests(TestCase): | ||
""" | ||
Tests for the retrieve_unsubscribed_emails command. | ||
""" | ||
|
||
@staticmethod | ||
def _write_test_csv(csv, lines): | ||
""" | ||
Write a test csv file with the lines provided | ||
""" | ||
csv.write(b"email,unsubscribed_at\n") | ||
for line in lines: | ||
csv.write(six.b(line)) | ||
csv.seek(0) | ||
return csv | ||
|
||
@override_settings( | ||
UNSUBSCRIBED_EMAILS_FROM_EMAIL='test@example.com', | ||
UNSUBSCRIBED_EMAILS_RECIPIENT_EMAIL=['test@example.com'] | ||
) | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.EmailMultiAlternatives.send') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.get_braze_client') | ||
def test_retrieve_unsubscribed_emails_command(self, mock_get_braze_client, mock_send): | ||
""" | ||
Test the retrieve_unsubscribed_emails command | ||
""" | ||
mock_braze_client = mock_get_braze_client.return_value | ||
mock_braze_client.retrieve_unsubscribed_emails.return_value = [ | ||
{'email': 'test1@example.com', 'unsubscribed_at': '2023-06-01 10:00:00'}, | ||
{'email': 'test2@example.com', 'unsubscribed_at': '2023-06-02 12:00:00'}, | ||
] | ||
mock_send.return_value = MagicMock() | ||
|
||
output = StringIO() | ||
call_command('retrieve_unsubscribed_emails', stdout=output) | ||
|
||
self.assertIn('Unsubscribed emails retrieved successfully', output.getvalue()) | ||
mock_send.assert_called_once() | ||
|
||
with NamedTemporaryFile(delete=False) as csv: | ||
filepath = csv.name | ||
lines = [ | ||
'test1@example.com,2023-06-01 10:00:00', | ||
'test2@example.com,2023-06-02 12:00:00' | ||
] | ||
self._write_test_csv(csv, lines) | ||
|
||
with open(filepath, 'r') as csv_file: | ||
csv_data = csv_file.read() | ||
self.assertIn('test1@example.com,2023-06-01 10:00:00', csv_data) | ||
self.assertIn('test2@example.com,2023-06-02 12:00:00', csv_data) | ||
|
||
os.remove(filepath) | ||
|
||
@override_settings( | ||
UNSUBSCRIBED_EMAILS_FROM_EMAIL='test@example.com', | ||
UNSUBSCRIBED_EMAILS_RECIPIENT_EMAIL=['test@example.com'] | ||
) | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.EmailMultiAlternatives.send') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.get_braze_client') | ||
def test_retrieve_unsubscribed_emails_command_with_dates(self, mock_get_braze_client, mock_send): | ||
""" | ||
Test the retrieve_unsubscribed_emails command with custom start and end dates. | ||
""" | ||
mock_braze_client = mock_get_braze_client.return_value | ||
mock_braze_client.retrieve_unsubscribed_emails.return_value = [ | ||
{'email': 'test3@example.com', 'unsubscribed_at': '2023-06-03 08:00:00'}, | ||
{'email': 'test4@example.com', 'unsubscribed_at': '2023-06-04 14:00:00'}, | ||
] | ||
mock_send.return_value = MagicMock() | ||
|
||
output = StringIO() | ||
call_command( | ||
'retrieve_unsubscribed_emails', | ||
'--start_date', '2023-06-03', | ||
'--end_date', '2023-06-04', | ||
stdout=output | ||
) | ||
|
||
self.assertIn('Unsubscribed emails retrieved successfully', output.getvalue()) | ||
mock_send.assert_called_once() | ||
|
||
with NamedTemporaryFile(delete=False) as csv: | ||
filepath = csv.name | ||
lines = [ | ||
'test3@example.com,2023-06-03 08:00:00', | ||
'test4@example.com,2023-06-04 14:00:00' | ||
] | ||
self._write_test_csv(csv, lines) | ||
|
||
with open(filepath, 'r') as csv_file: | ||
csv_data = csv_file.read() | ||
self.assertIn('test3@example.com,2023-06-03 08:00:00', csv_data) | ||
self.assertIn('test4@example.com,2023-06-04 14:00:00', csv_data) | ||
|
||
os.remove(filepath) | ||
|
||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.EmailMultiAlternatives.send') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.get_braze_client') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.logger.exception') | ||
def test_retrieve_unsubscribed_emails_command_exception(self, mock_logger_exception, mock_get_braze_client, | ||
mock_send): | ||
""" | ||
Test the retrieve_unsubscribed_emails command when an exception is raised. | ||
""" | ||
mock_braze_client = mock_get_braze_client.return_value | ||
mock_braze_client.retrieve_unsubscribed_emails.side_effect = Exception('Braze API error') | ||
mock_send.return_value = MagicMock() | ||
|
||
output = StringIO() | ||
with self.assertRaises(CommandError): | ||
call_command('retrieve_unsubscribed_emails', stdout=output) | ||
|
||
mock_logger_exception.assert_called_once_with( | ||
'Unable to retrieve unsubscribed emails from Braze due to exception: Braze API error' | ||
) | ||
mock_send.assert_not_called() | ||
|
||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.EmailMultiAlternatives.send') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.get_braze_client') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.logger.info') | ||
def test_retrieve_unsubscribed_emails_command_no_data(self, mock_logger_info, mock_get_braze_client, mock_send): | ||
""" | ||
Test the retrieve_unsubscribed_emails command when no unsubscribed emails are returned. | ||
""" | ||
mock_braze_client = mock_get_braze_client.return_value | ||
mock_braze_client.retrieve_unsubscribed_emails.return_value = [] | ||
mock_send.return_value = MagicMock() | ||
|
||
output = StringIO() | ||
call_command('retrieve_unsubscribed_emails', stdout=output) | ||
|
||
self.assertIn('Unsubscribed emails retrieved successfully', output.getvalue()) | ||
mock_send.assert_not_called() | ||
mock_logger_info.assert_called_once_with( | ||
'No unsubscribed emails found within the specified date range.' | ||
) | ||
|
||
@override_settings( | ||
UNSUBSCRIBED_EMAILS_FROM_EMAIL='test@example.com', | ||
UNSUBSCRIBED_EMAILS_RECIPIENT_EMAIL=['test@example.com'] | ||
) | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.EmailMultiAlternatives.send') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.get_braze_client') | ||
@patch('common.djangoapps.student.management.commands.retrieve_unsubscribed_emails.logger.exception') | ||
def test_retrieve_unsubscribed_emails_command_error_sending_email(self, mock_logger_exception, | ||
mock_get_braze_client, mock_send): | ||
""" | ||
Test the retrieve_unsubscribed_emails command when an error occurs during email sending. | ||
""" | ||
mock_braze_client = mock_get_braze_client.return_value | ||
mock_braze_client.retrieve_unsubscribed_emails.return_value = [ | ||
{'email': 'test1@example.com', 'unsubscribed_at': '2023-06-01 10:00:00'}, | ||
] | ||
mock_send.side_effect = Exception('Email sending error') | ||
|
||
output = StringIO() | ||
|
||
with self.assertRaises(CommandError): | ||
call_command('retrieve_unsubscribed_emails', stdout=output) | ||
|
||
mock_logger_exception.assert_called_once_with( | ||
'Failure to send unsubscribed emails data to your email due to exception: Email sending error' | ||
) |
14 changes: 14 additions & 0 deletions
14
common/djangoapps/student/templates/unsubscribed_emails/email/email_base.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{% load i18n %} | ||
{% get_current_language as LANGUAGE_CODE %} | ||
<!DOCTYPE html> | ||
<html lang="{{ LANGUAGE_CODE }}"> | ||
<head> | ||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | ||
</head> | ||
<body style="font-family:Arial,'Helvetica Neue',Helvetica,sans-serif;font-size:14px;line-height:150%;margin:auto"> | ||
|
||
{% block body %} | ||
{% endblock body %} | ||
|
||
</body> | ||
</html> |
8 changes: 8 additions & 0 deletions
8
common/djangoapps/student/templates/unsubscribed_emails/email/email_body.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{% extends "unsubscribed_emails/email/email_base.html" %} | ||
{% block body %} | ||
|
||
<p> | ||
Please find attached CSV file for Unsubscribed Emails from <strong>{{ start_date }}</strong> to <strong>{{ end_date }}</strong> | ||
</p> | ||
|
||
{% endblock body %} |
1 change: 1 addition & 0 deletions
1
common/djangoapps/student/templates/unsubscribed_emails/email/email_body.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Please find attached CSV file for Unsubscribed Emails from {{ start_date }} to {{ end_date }} |