Skip to content

Commit

Permalink
feat: add a command to fetch unsubscribed emails from Braze
Browse files Browse the repository at this point in the history
  • Loading branch information
shahbaz-shabbir05 committed Jul 6, 2023
1 parent 74e3bb9 commit 4dca42a
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
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.')
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'
)
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>
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 %}
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 }}

0 comments on commit 4dca42a

Please sign in to comment.