From 00643904fa0f2b87364b0e6684781b9d5e6f9baf Mon Sep 17 00:00:00 2001 From: Jawad Khan Date: Tue, 17 Jun 2025 04:50:45 +0500 Subject: [PATCH 1/5] fix: Moved braze push channel to edx-braze-client --- edx_ace/__init__.py | 2 +- edx_ace/channel/__init__.py | 10 +- edx_ace/channel/braze_push_notification.py | 64 ----------- edx_ace/presentation.py | 1 - edx_ace/renderers.py | 15 --- .../channel/test_braze_push_notification.py | 105 ------------------ edx_ace/tests/test_policy.py | 11 +- edx_ace/utils/braze.py | 27 ----- requirements/dev.in | 1 - requirements/dev.txt | 8 +- requirements/doc.in | 1 - requirements/doc.txt | 8 +- requirements/test.in | 1 - requirements/test.txt | 8 +- setup.py | 2 - 15 files changed, 14 insertions(+), 250 deletions(-) delete mode 100644 edx_ace/channel/braze_push_notification.py delete mode 100644 edx_ace/tests/channel/test_braze_push_notification.py delete mode 100644 edx_ace/utils/braze.py diff --git a/edx_ace/__init__.py b/edx_ace/__init__.py index 66aacefc..a8b8f0fb 100644 --- a/edx_ace/__init__.py +++ b/edx_ace/__init__.py @@ -13,7 +13,7 @@ from .recipient import Recipient from .recipient_resolver import RecipientResolver -__version__ = '1.14.0' +__version__ = '1.15.0' __all__ = [ diff --git a/edx_ace/channel/__init__.py b/edx_ace/channel/__init__.py index 848fc4b2..5c16bce3 100644 --- a/edx_ace/channel/__init__.py +++ b/edx_ace/channel/__init__.py @@ -28,7 +28,6 @@ class ChannelType(Enum): EMAIL = 'email' PUSH = 'push' - BRAZE_PUSH = 'braze_push' def __str__(self): return str(self.value) @@ -183,10 +182,11 @@ def get_channel_for_message(channel_type, message): channel_names = [settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL, settings.ACE_CHANNEL_DEFAULT_EMAIL] else: channel_names = [settings.ACE_CHANNEL_DEFAULT_EMAIL] - elif channel_type == ChannelType.PUSH and getattr(settings, "ACE_CHANNEL_DEFAULT_PUSH", None): - channel_names = [settings.ACE_CHANNEL_DEFAULT_PUSH] - elif channel_type == ChannelType.BRAZE_PUSH and getattr(settings, "ACE_CHANNEL_BRAZE_PUSH", None): - channel_names = [settings.ACE_CHANNEL_BRAZE_PUSH] + elif channel_type == ChannelType.PUSH: + if getattr(settings, "ACE_CHANNEL_DEFAULT_PUSH", None): + channel_names = [settings.ACE_CHANNEL_DEFAULT_PUSH] + elif getattr(settings, "ACE_PUSH_CHANNELS", []): + channel_names = settings.ACE_PUSH_CHANNELS try: possible_channels = [ diff --git a/edx_ace/channel/braze_push_notification.py b/edx_ace/channel/braze_push_notification.py deleted file mode 100644 index a4f74c1b..00000000 --- a/edx_ace/channel/braze_push_notification.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Channel for sending push notifications using braze. -""" -import logging - -from django.conf import settings - -from edx_ace.channel import Channel, ChannelType -from edx_ace.message import Message -from edx_ace.renderers import RenderedPushNotification -from edx_ace.utils.braze import get_braze_client - -LOG = logging.getLogger(__name__) - - -class BrazePushNotificationChannel(Channel): - """ - A channel for sending push notifications using braze. - """ - channel_type = ChannelType.BRAZE_PUSH - _CAMPAIGNS_SETTING = 'ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS' - - @classmethod - def enabled(cls): - """ - Returns: True iff braze client is available. - """ - return bool(get_braze_client()) - - def deliver(self, message: Message, rendered_message: RenderedPushNotification) -> None: - """ - Transmit a rendered message to a recipient. - - Args: - message: The message to transmit. - rendered_message: The rendered content of the message that has been personalized - for this particular recipient. - """ - braze_campaign = message.options['braze_campaign'] - emails = message.options.get('emails') or [message.recipient.email_address] - campaign_id = self._campaign_id(braze_campaign) - if not campaign_id: - LOG.info('Could not find braze campaign for notification %s', braze_campaign) - return - - try: - braze_client = get_braze_client() - braze_client.send_campaign_message( - campaign_id=campaign_id, - trigger_properties=message.context['post_data'], - emails=emails - ) - LOG.info('Sent push notification for %s with Braze', braze_campaign) - except Exception as exc: # pylint: disable=broad-except - LOG.error( - 'Unable to send push notification for %s with Braze. Reason: %s', - braze_campaign, - str(exc) - ) - - @classmethod - def _campaign_id(cls, braze_campaign): - """Returns the campaign ID for a given ACE message name or None if no match is found""" - return getattr(settings, cls._CAMPAIGNS_SETTING, {}).get(braze_campaign) diff --git a/edx_ace/presentation.py b/edx_ace/presentation.py index c7131917..cde5829a 100644 --- a/edx_ace/presentation.py +++ b/edx_ace/presentation.py @@ -10,7 +10,6 @@ RENDERERS = { ChannelType.EMAIL: renderers.EmailRenderer(), ChannelType.PUSH: renderers.PushNotificationRenderer(), - ChannelType.BRAZE_PUSH: renderers.BrazePushNotificationRenderer(), } diff --git a/edx_ace/renderers.py b/edx_ace/renderers.py index 03b5de32..53716e0e 100644 --- a/edx_ace/renderers.py +++ b/edx_ace/renderers.py @@ -99,18 +99,3 @@ class PushNotificationRenderer(AbstractRenderer): A renderer for :attr:`.ChannelType.PUSH` channels. """ rendered_message_cls = RenderedPushNotification - - -@attr.s -class RenderedBrazePushNotification: - """ - Encapsulates all values needed to send a :class:`.Message` - over an :attr:`.ChannelType.BRAZE_PUSH`. - """ - - -class BrazePushNotificationRenderer(AbstractRenderer): - """ - A renderer for :attr:`.ChannelType.PUSH` channels. - """ - rendered_message_cls = RenderedBrazePushNotification diff --git a/edx_ace/tests/channel/test_braze_push_notification.py b/edx_ace/tests/channel/test_braze_push_notification.py deleted file mode 100644 index 8f3776ae..00000000 --- a/edx_ace/tests/channel/test_braze_push_notification.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Tests for TestBrazePushNotificationChannel. -""" -from unittest.mock import MagicMock, patch - -import pytest - -from django.contrib.auth import get_user_model -from django.test import TestCase, override_settings - -from edx_ace.channel.braze_push_notification import BrazePushNotificationChannel -from edx_ace.message import Message -from edx_ace.recipient import Recipient -from edx_ace.renderers import RenderedBrazePushNotification - -BRAZE_URL = "https://example.braze.com" -API_KEY = "test-api-key" -User = get_user_model() - - -@pytest.mark.django_db -@override_settings( - EDX_BRAZE_API_KEY=API_KEY, - EDX_BRAZE_API_SERVER=BRAZE_URL, -) -class TestBrazePushNotificationChannel(TestCase): - - def setUp(self): - super().setUp() - self.user = User.objects.create(username='username', email='email@example.com') - self.lms_user_id = self.user.id - self.mocked_post_data = { - 'notification_type': 'new_response', - 'course_id': 'course-v1:edX+DemoX+Demo_Course', - 'content_url': 'http://localhost', - 'replier_name': 'verified', - 'post_title': 'New test response', - 'course_name': 'Demonstration Course', - 'thread_id': '67bedeb9ceb0b101343294c5', - 'topic_id': 'i4x-edx-eiorguegnru-course-foobarbaz', - 'response_id': '67ffa1f1ceb0b10134db3d8e', - 'comment_id': None, - 'strong': 'strong', 'p': 'p' - } - - self.mocked_payload = { - 'campaign_id': '1234test', - 'trigger_properties': self.mocked_post_data, - 'emails': ['edx@example.com'] - } - - @patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=True) - def test_enabled(self, mock_braze_client): - """ - Test that the channel is enabled when the settings are configured. - """ - assert BrazePushNotificationChannel.enabled() - - @patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=False) - def test_disabled(self, mock_braze_client): - """ - Test that the channel is disabled when the settings are not configured. - """ - assert not BrazePushNotificationChannel.enabled() - - @override_settings(ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS={'new_response': "1234test"}) - @patch('edx_ace.channel.braze_push_notification.get_braze_client') - def test_deliver_success(self, mock_braze_function): - mock_braze_client = MagicMock() - mock_braze_function.return_value = mock_braze_client - mock_braze_client.send_campaign_message = MagicMock(return_value=True) - - rendered_message = RenderedBrazePushNotification() - message = Message( - app_label='testapp', - name="test_braze", - recipient=Recipient(lms_user_id="1", email_address="user@example.com"), - context={'post_data': self.mocked_post_data}, - options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'} - ) - channel = BrazePushNotificationChannel() - channel.deliver(message, rendered_message) - mock_braze_client.send_campaign_message.assert_called_once() - args, kwargs = mock_braze_client.send_campaign_message.call_args - - # Verify the payload - self.assertEqual(kwargs, self.mocked_payload) - - @patch('edx_ace.channel.braze_push_notification.get_braze_client') - def test_campaign_not_configured(self, mock_braze_function): - mock_braze_client = MagicMock() - mock_braze_function.return_value = mock_braze_client - mock_braze_client.send_campaign_message = MagicMock(return_value=True) - - rendered_message = RenderedBrazePushNotification() - message = Message( - app_label='testapp', - name="test_braze", - recipient=Recipient(lms_user_id="1", email_address="user@example.com"), - context={'post_data': self.mocked_post_data}, - options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'} - ) - channel = BrazePushNotificationChannel() - channel.deliver(message, rendered_message) - mock_braze_client.send_campaign_message.assert_not_called() diff --git a/edx_ace/tests/test_policy.py b/edx_ace/tests/test_policy.py index 7fcf091a..87f91087 100644 --- a/edx_ace/tests/test_policy.py +++ b/edx_ace/tests/test_policy.py @@ -25,15 +25,14 @@ class TestPolicy(TestCase): # deny only email PolicyCase(deny_values=[{ChannelType.EMAIL}], - expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # single policy + expected_channels={ChannelType.PUSH}), # single policy PolicyCase(deny_values=[{ChannelType.EMAIL}, set()], - expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # multiple policies + expected_channels={ChannelType.PUSH}), # multiple policies - # deny email, push and braze_push - PolicyCase(deny_values=[{ChannelType.EMAIL, ChannelType.PUSH, ChannelType.BRAZE_PUSH}], + # deny email and push + PolicyCase(deny_values=[{ChannelType.EMAIL, ChannelType.PUSH}], expected_channels=set()), # single policy - PolicyCase(deny_values=[{ChannelType.EMAIL}, {ChannelType.PUSH}, - {ChannelType.BRAZE_PUSH}], expected_channels=set()), # multiple policies + PolicyCase(deny_values=[{ChannelType.EMAIL}, {ChannelType.PUSH}], expected_channels=set()), # multiple policies # deny all and email PolicyCase(deny_values=[{ChannelType.EMAIL}, set(ChannelType)], expected_channels=set()), diff --git a/edx_ace/utils/braze.py b/edx_ace/utils/braze.py deleted file mode 100644 index e71ab91e..00000000 --- a/edx_ace/utils/braze.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -Helper Methods related to braze client -""" - -try: - from braze.client import BrazeClient -except ImportError: - BrazeClient = None -from django.conf import settings - - -def get_braze_client(): - """ Returns a Braze client. """ - if not BrazeClient: - return None - - braze_api_key = getattr(settings, 'ACE_CHANNEL_BRAZE_PUSH_API_KEY', None) - braze_api_url = getattr(settings, 'ACE_CHANNEL_BRAZE_REST_ENDPOINT', None) - - if not braze_api_key or not braze_api_url: - return None - - return BrazeClient( - api_key=braze_api_key, - api_url=f"https://{braze_api_url}", - app_id='', - ) diff --git a/requirements/dev.in b/requirements/dev.in index ee80e5ac..5f086dc9 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -10,4 +10,3 @@ tox # virtualenv management for tests twine # Utility for PyPI package uploads wheel # For generation of wheels for PyPI backports.zoneinfo; python_version<'3.9' # Needed for Python 3.12 compatibility -edx-braze-client==1.0.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index 80c8a51b..8261a63f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -60,7 +60,6 @@ django==4.2.20 # django-crum # django-push-notifications # django-waffle - # edx-braze-client # edx-django-utils # edx-i18n-tools django-crum==0.7.9 @@ -71,12 +70,8 @@ django-waffle==4.2.0 # via edx-django-utils docutils==0.21.2 # via readme-renderer -edx-braze-client==1.0.2 - # via -r requirements/dev.in edx-django-utils==7.4.0 - # via - # -r requirements/base.in - # edx-braze-client + # via -r requirements/base.in edx-i18n-tools==1.8.0 # via -r requirements/dev.in edx-lint==5.6.0 @@ -272,7 +267,6 @@ readme-renderer==44.0 requests==2.32.3 # via # cachecontrol - # edx-braze-client # google-api-core # google-cloud-storage # id diff --git a/requirements/doc.in b/requirements/doc.in index f3801aae..0d78a715 100644 --- a/requirements/doc.in +++ b/requirements/doc.in @@ -8,4 +8,3 @@ readme_renderer # Validates README.rst for usage on PyPI Sphinx # Documentation builder twine backports.zoneinfo; python_version<'3.9' # Needed for Python 3.12 compatibility -edx-braze-client==1.0.2 diff --git a/requirements/doc.txt b/requirements/doc.txt index 63901d04..dfed43fa 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -41,7 +41,6 @@ django==4.2.20 # django-crum # django-push-notifications # django-waffle - # edx-braze-client # edx-django-utils django-crum==0.7.9 # via edx-django-utils @@ -58,12 +57,8 @@ docutils==0.21.2 # readme-renderer # restructuredtext-lint # sphinx -edx-braze-client==1.0.2 - # via -r requirements/doc.in edx-django-utils==7.4.0 - # via - # -r requirements/base.in - # edx-braze-client + # via -r requirements/base.in firebase-admin==6.8.0 # via -r requirements/base.in google-api-core[grpc]==2.24.2 @@ -198,7 +193,6 @@ readme-renderer==44.0 requests==2.32.3 # via # cachecontrol - # edx-braze-client # google-api-core # google-cloud-storage # id diff --git a/requirements/test.in b/requirements/test.in index fd674516..51678531 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -11,4 +11,3 @@ pudb # For easier test debugging hypothesis[pytz] # For property-based testing hypothesis-pytest backports.zoneinfo; python_version<'3.9' # Needed for Python 3.12 compatibility -edx-braze-client==1.0.2 diff --git a/requirements/test.txt b/requirements/test.txt index 7d229d23..f8dfcbd1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -36,7 +36,6 @@ ddt==1.7.2 # django-crum # django-push-notifications # django-waffle - # edx-braze-client # edx-django-utils django-crum==0.7.9 # via edx-django-utils @@ -44,12 +43,8 @@ django-push-notifications==3.2.1 # via -r requirements/base.in django-waffle==4.2.0 # via edx-django-utils -edx-braze-client==1.0.2 - # via -r requirements/test.in edx-django-utils==7.4.0 - # via - # -r requirements/base.in - # edx-braze-client + # via -r requirements/base.in firebase-admin==6.8.0 # via -r requirements/base.in google-api-core[grpc]==2.24.2 @@ -177,7 +172,6 @@ pytz==2025.2 requests==2.32.3 # via # cachecontrol - # edx-braze-client # google-api-core # google-cloud-storage # sailthru-client diff --git a/setup.py b/setup.py index e14c23bb..81c0379c 100644 --- a/setup.py +++ b/setup.py @@ -143,7 +143,6 @@ def is_requirement(line): extras_require={ 'sailthru': ["sailthru-client>2.2,<2.3"], 'push_notifications': ["django-push-notifications[FCM]"], - 'braze_push': ['edx-braze-client==1.0.2'] }, license="AGPL 3.0", zip_safe=False, @@ -166,7 +165,6 @@ def is_requirement(line): 'file_email = edx_ace.channel.file:FileEmailChannel', 'django_email = edx_ace.channel.django_email:DjangoEmailChannel', 'push_notification = edx_ace.channel.push_notification:PushNotificationChannel', - 'braze_push = edx_ace.channel.braze_push_notification:BrazePushNotificationChannel', ] } ) From fdebe7ccf596afb8a78af8988d6f91e2edbb4dd6 Mon Sep 17 00:00:00 2001 From: Jawad Khan Date: Tue, 17 Jun 2025 04:58:02 +0500 Subject: [PATCH 2/5] fix: removed unwanted test file --- edx_ace/tests/utils/test_braze_utils.py | 59 ------------------------- 1 file changed, 59 deletions(-) delete mode 100644 edx_ace/tests/utils/test_braze_utils.py diff --git a/edx_ace/tests/utils/test_braze_utils.py b/edx_ace/tests/utils/test_braze_utils.py deleted file mode 100644 index 6ea00a45..00000000 --- a/edx_ace/tests/utils/test_braze_utils.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Test cases for utils.braze -""" -from unittest.mock import patch - -import pytest - -from django.contrib.auth import get_user_model -from django.test import TestCase, override_settings - -from edx_ace.utils.braze import get_braze_client - -BRAZE_URL = "https://example.braze.com" -API_KEY = "test-api-key" -User = get_user_model() - - -@pytest.mark.django_db -class TestBrazeClient(TestCase): - """ Test cases for utils.braze """ - - @patch('edx_ace.utils.braze.BrazeClient') - def test_disabled(self, mock_braze_client): - """ - Test that the channel is settings aren't configured. - """ - result = get_braze_client() - self.assertEqual(result, None) - mock_braze_client.assert_not_called() - - @override_settings(ACE_CHANNEL_BRAZE_PUSH_API_KEY=API_KEY) - @patch('edx_ace.utils.braze.BrazeClient') - def test_braze_url_not_configured(self, mock_braze_client): - """ - Test that the channel is settings aren't configured. - """ - result = get_braze_client() - self.assertEqual(result, None) - mock_braze_client.assert_not_called() - - @override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY) - @patch('edx_ace.utils.braze.BrazeClient') - def test_braze_api_key_not_configured(self, mock_braze_client): - """ - Test that the channel is settings aren't configured. - """ - result = get_braze_client() - self.assertEqual(result, None) - mock_braze_client.assert_not_called() - - @override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY, ACE_CHANNEL_BRAZE_PUSH_API_KEY=API_KEY) - @patch('edx_ace.utils.braze.BrazeClient', return_value=True) - def test_success(self, mock_braze_client): - """ - Test that the channel is settings aren't configured. - """ - result = get_braze_client() - self.assertEqual(result, True) - mock_braze_client.assert_called_once() From 8ccf0bbc7a2f5cbe17c50e972dd1b17742ea423d Mon Sep 17 00:00:00 2001 From: Jawad Khan Date: Sat, 21 Jun 2025 09:01:15 +0500 Subject: [PATCH 3/5] fix!: Removed braze-push channel from repo --- CHANGELOG.rst | 6 ++++++ setup.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 64de1e36..b502725e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,12 @@ Change Log Unreleased ********** +[1.15.0] - 2025-04-25 +--------------------- + +* Removed edx-braze-client from repo +* Removed braze_push channel from repo + [1.13.0] - 2025-04-25 --------------------- diff --git a/setup.py b/setup.py index 81c0379c..a3e01239 100644 --- a/setup.py +++ b/setup.py @@ -160,6 +160,10 @@ def is_requirement(line): ], entry_points={ 'openedx.ace.channel': [ + # These should be generic, non-vendor-specific channels. + # If you have vendor-specific channels, you can add them using this entrypoint, + # but please do so in a separate plugin repository. The braze_email and sailthru_email + # channels listed above were added before this rule; they are not a pattern to follow. 'braze_email = edx_ace.channel.braze:BrazeEmailChannel', 'sailthru_email = edx_ace.channel.sailthru:SailthruEmailChannel', 'file_email = edx_ace.channel.file:FileEmailChannel', From 6825838dcfb7b58b0b1f9802c29425c7f84c027a Mon Sep 17 00:00:00 2001 From: Jawad Khan Date: Tue, 24 Jun 2025 16:59:20 +0500 Subject: [PATCH 4/5] fix: fix invalid uuid issue in test case --- edx_ace/serialization.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/edx_ace/serialization.py b/edx_ace/serialization.py index 9f5fe652..e84a6d14 100644 --- a/edx_ace/serialization.py +++ b/edx_ace/serialization.py @@ -92,7 +92,10 @@ def _deserialize_field(cls, field_name, field_value): if field_name == 'expiration_time': return date.deserialize(field_value) elif field_name in ('uuid', 'send_uuid'): - return UUID(field_value) + try: + return UUID(field_value) + except ValueError: + return field_value # TODO(later): should this be more dynamic? elif field_name == 'message': return Message(**field_value) From 87943b5ace5bcf4a5205058e67475241ceb0f0bf Mon Sep 17 00:00:00 2001 From: Jawad Khan Date: Tue, 24 Jun 2025 17:37:07 +0500 Subject: [PATCH 5/5] fix: fixed requirements issue --- edx_ace/serialization.py | 5 +---- requirements/test.txt | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/edx_ace/serialization.py b/edx_ace/serialization.py index e84a6d14..9f5fe652 100644 --- a/edx_ace/serialization.py +++ b/edx_ace/serialization.py @@ -92,10 +92,7 @@ def _deserialize_field(cls, field_name, field_value): if field_name == 'expiration_time': return date.deserialize(field_value) elif field_name in ('uuid', 'send_uuid'): - try: - return UUID(field_value) - except ValueError: - return field_value + return UUID(field_value) # TODO(later): should this be more dynamic? elif field_name == 'message': return Message(**field_value) diff --git a/requirements/test.txt b/requirements/test.txt index f8dfcbd1..87e37496 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -94,7 +94,7 @@ httplib2==0.22.0 # via # google-api-python-client # google-auth-httplib2 -hypothesis[pytz]==6.131.9 +hypothesis[pytz]==6.104.2 # via # -r requirements/test.in # hypothesis-pytest