feat: Add management command to unsubscribe user email (#31705)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""Management command to unsubscribe user's email in bulk on Braze."""
|
||||
import csv
|
||||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from lms.djangoapps.utils import get_braze_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CHUNK_SIZE = 50
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Management command to unsubscribe user's email in bulk on Braze.
|
||||
"""
|
||||
|
||||
help = """
|
||||
Unsubscribe for all given user's email on braze.
|
||||
|
||||
Example:
|
||||
|
||||
Unsubscribe user's email for multiple users on braze.
|
||||
$ ... unsubscribe_user_email -p <csv_file_path>
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'-p', '--csv_path',
|
||||
metavar='csv_path',
|
||||
dest='csv_path',
|
||||
required=False,
|
||||
help='Path to CSV file.')
|
||||
|
||||
def _chunked_iterable(self, iterable):
|
||||
"""
|
||||
Yield successive CHUNK_SIZE sized chunks from iterable.
|
||||
"""
|
||||
for i in range(0, len(iterable), CHUNK_SIZE):
|
||||
yield iterable[i:i + CHUNK_SIZE]
|
||||
|
||||
def _chunk_list(self, emails_list):
|
||||
"""
|
||||
Chunk a list into sub-lists of length CHUNK_SIZE.
|
||||
"""
|
||||
return list(self._chunked_iterable(emails_list))
|
||||
|
||||
def handle(self, *args, **options):
|
||||
emails = []
|
||||
csv_file_path = options['csv_path']
|
||||
|
||||
try:
|
||||
with open(csv_file_path, 'r') as csv_file:
|
||||
reader = list(csv.DictReader(csv_file))
|
||||
emails = [row.get('email') for row in reader]
|
||||
except FileNotFoundError as exc:
|
||||
raise CommandError(f"Error: File not found due to exception - {exc}") # lint-amnesty, pylint: disable=raise-missing-from
|
||||
except csv.Error as exc:
|
||||
logger.exception(f"CSV error: {exc}")
|
||||
else:
|
||||
logger.info("CSV file read successfully.")
|
||||
|
||||
chunks = self._chunk_list(emails)
|
||||
|
||||
try:
|
||||
braze_client = get_braze_client()
|
||||
if braze_client:
|
||||
for i, chunk in enumerate(chunks):
|
||||
braze_client.unsubscribe_user_email(
|
||||
email=chunk,
|
||||
)
|
||||
logger.info(f"Successfully unsubscribed for chunk-{i + 1} consist of {len(chunk)} emails")
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.exception(f"Unable to update email status on Braze due to : {exc}")
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Tests for unsubscribe user's email management command"""
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import six
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.test import TestCase
|
||||
|
||||
COMMAND = 'unsubscribe_user_email'
|
||||
|
||||
|
||||
class UnsubscribeUserEmailTests(TestCase):
|
||||
"""
|
||||
Tests unsubscribe_user_email command
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up tests
|
||||
"""
|
||||
super().setUp()
|
||||
|
||||
self.lines = [
|
||||
f"test_user{i}@test.com" for i in range(100)
|
||||
]
|
||||
self.invalid_csv_path = '/test/test.csv'
|
||||
|
||||
@staticmethod
|
||||
def _write_test_csv(csv, lines):
|
||||
"""Write a test csv file with the lines provided"""
|
||||
|
||||
csv.write(b"email\n")
|
||||
for line in lines:
|
||||
csv.write(six.b(line))
|
||||
csv.seek(0)
|
||||
return csv
|
||||
|
||||
@patch("common.djangoapps.student.management.commands.unsubscribe_user_email.get_braze_client")
|
||||
def test_unsubscribe_user_email(self, mock_get_braze_client):
|
||||
""" Test CSV file to unsubscribe user's email"""
|
||||
|
||||
with NamedTemporaryFile() as csv:
|
||||
csv = self._write_test_csv(csv, self.lines)
|
||||
|
||||
call_command(
|
||||
COMMAND,
|
||||
'--csv_path',
|
||||
csv.name
|
||||
)
|
||||
|
||||
mock_get_braze_client.assert_called_once()
|
||||
|
||||
def test_command_error_for_csv_path(self):
|
||||
""" Test command error raised if csv_path is not valid"""
|
||||
|
||||
with pytest.raises(CommandError):
|
||||
call_command(
|
||||
COMMAND,
|
||||
'--csv_path',
|
||||
self.invalid_csv_path
|
||||
)
|
||||
Reference in New Issue
Block a user