From ff0805a1894ccfd3e312fcaff221efd60a45044a Mon Sep 17 00:00:00 2001 From: Shahbaz Shabbir <32649010+shahbaz-arbisoft@users.noreply.github.com> Date: Wed, 1 Mar 2023 18:23:56 +0500 Subject: [PATCH] feat: Add management command to unsubscribe user email (#31705) --- .../commands/unsubscribe_user_email.py | 74 +++++++++++++++++++ .../tests/test_unsubscribe_user_email.py | 64 ++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 common/djangoapps/student/management/commands/unsubscribe_user_email.py create mode 100644 common/djangoapps/student/management/tests/test_unsubscribe_user_email.py diff --git a/common/djangoapps/student/management/commands/unsubscribe_user_email.py b/common/djangoapps/student/management/commands/unsubscribe_user_email.py new file mode 100644 index 0000000000..23973b8475 --- /dev/null +++ b/common/djangoapps/student/management/commands/unsubscribe_user_email.py @@ -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 + """ + + 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}") diff --git a/common/djangoapps/student/management/tests/test_unsubscribe_user_email.py b/common/djangoapps/student/management/tests/test_unsubscribe_user_email.py new file mode 100644 index 0000000000..979943ac9a --- /dev/null +++ b/common/djangoapps/student/management/tests/test_unsubscribe_user_email.py @@ -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 + )