edx-ace 1.11.4__tar.gz → 1.12.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {edx-ace-1.11.4/edx_ace.egg-info → edx-ace-1.12.1}/PKG-INFO +1 -1
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/__init__.py +1 -1
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/__init__.py +3 -0
- edx-ace-1.12.1/edx_ace/channel/braze_push_notification.py +64 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/presentation.py +1 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/renderers.py +15 -0
- edx-ace-1.12.1/edx_ace/tests/channel/test_braze_push_notification.py +105 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_policy.py +10 -6
- edx-ace-1.12.1/edx_ace/tests/utils/test_braze_utils.py +59 -0
- edx-ace-1.12.1/edx_ace/utils/braze.py +27 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1/edx_ace.egg-info}/PKG-INFO +1 -1
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/SOURCES.txt +4 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/entry_points.txt +1 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/requires.txt +1 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/requirements/base.in +1 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/setup.py +36 -6
- {edx-ace-1.11.4 → edx-ace-1.12.1}/CHANGELOG.rst +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/LICENSE.txt +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/MANIFEST.in +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/README.rst +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/ace.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/apps.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/braze.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/django_email.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/file.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/mixins.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/push_notification.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/channel/sailthru.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/delivery.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/errors.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/message.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/monitoring.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/policy.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/push_notifications/views/__init__.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/recipient.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/recipient_resolver.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/serialization.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/signals.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/templatetags/acetags.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/test_utils/__init__.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_braze.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_channel_helpers.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_django_email.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_file_email.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_push_notification.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/channel/test_sailthru.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_ace.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_date.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_delivery.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_message.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_presentation.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/body.html +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/head.html +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/tests/utils/test_signals.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/utils/__init__.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/utils/date.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/utils/once.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/utils/plugins.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace/utils/signals.py +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/dependency_links.txt +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/not-zip-safe +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/edx_ace.egg-info/top_level.txt +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/requirements/constraints.txt +0 -0
- {edx-ace-1.11.4 → edx-ace-1.12.1}/setup.cfg +0 -0
@@ -28,6 +28,7 @@ class ChannelType(Enum):
|
|
28
28
|
|
29
29
|
EMAIL = 'email'
|
30
30
|
PUSH = 'push'
|
31
|
+
BRAZE_PUSH = 'braze_push'
|
31
32
|
|
32
33
|
def __str__(self):
|
33
34
|
return str(self.value)
|
@@ -184,6 +185,8 @@ def get_channel_for_message(channel_type, message):
|
|
184
185
|
channel_names = [settings.ACE_CHANNEL_DEFAULT_EMAIL]
|
185
186
|
elif channel_type == ChannelType.PUSH and getattr(settings, "ACE_CHANNEL_DEFAULT_PUSH", None):
|
186
187
|
channel_names = [settings.ACE_CHANNEL_DEFAULT_PUSH]
|
188
|
+
elif channel_type == ChannelType.BRAZE_PUSH and getattr(settings, "ACE_CHANNEL_BRAZE_PUSH", None):
|
189
|
+
channel_names = [settings.ACE_CHANNEL_BRAZE_PUSH]
|
187
190
|
|
188
191
|
try:
|
189
192
|
possible_channels = [
|
@@ -0,0 +1,64 @@
|
|
1
|
+
"""
|
2
|
+
Channel for sending push notifications using braze.
|
3
|
+
"""
|
4
|
+
import logging
|
5
|
+
|
6
|
+
from django.conf import settings
|
7
|
+
|
8
|
+
from edx_ace.channel import Channel, ChannelType
|
9
|
+
from edx_ace.message import Message
|
10
|
+
from edx_ace.renderers import RenderedPushNotification
|
11
|
+
from edx_ace.utils.braze import get_braze_client
|
12
|
+
|
13
|
+
LOG = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class BrazePushNotificationChannel(Channel):
|
17
|
+
"""
|
18
|
+
A channel for sending push notifications using braze.
|
19
|
+
"""
|
20
|
+
channel_type = ChannelType.BRAZE_PUSH
|
21
|
+
_CAMPAIGNS_SETTING = 'ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS'
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def enabled(cls):
|
25
|
+
"""
|
26
|
+
Returns: True iff braze client is available.
|
27
|
+
"""
|
28
|
+
return bool(get_braze_client())
|
29
|
+
|
30
|
+
def deliver(self, message: Message, rendered_message: RenderedPushNotification) -> None:
|
31
|
+
"""
|
32
|
+
Transmit a rendered message to a recipient.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
message: The message to transmit.
|
36
|
+
rendered_message: The rendered content of the message that has been personalized
|
37
|
+
for this particular recipient.
|
38
|
+
"""
|
39
|
+
braze_campaign = message.options['braze_campaign']
|
40
|
+
emails = message.options.get('emails') or [message.recipient.email_address]
|
41
|
+
campaign_id = self._campaign_id(braze_campaign)
|
42
|
+
if not campaign_id:
|
43
|
+
LOG.info('Could not find braze campaign for notification %s', braze_campaign)
|
44
|
+
return
|
45
|
+
|
46
|
+
try:
|
47
|
+
braze_client = get_braze_client()
|
48
|
+
braze_client.send_campaign_message(
|
49
|
+
campaign_id=campaign_id,
|
50
|
+
trigger_properties=message.context['post_data'],
|
51
|
+
emails=emails
|
52
|
+
)
|
53
|
+
LOG.info('Sent push notification for %s with Braze', braze_campaign)
|
54
|
+
except Exception as exc: # pylint: disable=broad-except
|
55
|
+
LOG.error(
|
56
|
+
'Unable to send push notification for %s with Braze. Reason: %s',
|
57
|
+
braze_campaign,
|
58
|
+
str(exc)
|
59
|
+
)
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def _campaign_id(cls, braze_campaign):
|
63
|
+
"""Returns the campaign ID for a given ACE message name or None if no match is found"""
|
64
|
+
return getattr(settings, cls._CAMPAIGNS_SETTING, {}).get(braze_campaign)
|
@@ -99,3 +99,18 @@ class PushNotificationRenderer(AbstractRenderer):
|
|
99
99
|
A renderer for :attr:`.ChannelType.PUSH` channels.
|
100
100
|
"""
|
101
101
|
rendered_message_cls = RenderedPushNotification
|
102
|
+
|
103
|
+
|
104
|
+
@attr.s
|
105
|
+
class RenderedBrazePushNotification:
|
106
|
+
"""
|
107
|
+
Encapsulates all values needed to send a :class:`.Message`
|
108
|
+
over an :attr:`.ChannelType.BRAZE_PUSH`.
|
109
|
+
"""
|
110
|
+
|
111
|
+
|
112
|
+
class BrazePushNotificationRenderer(AbstractRenderer):
|
113
|
+
"""
|
114
|
+
A renderer for :attr:`.ChannelType.PUSH` channels.
|
115
|
+
"""
|
116
|
+
rendered_message_cls = RenderedBrazePushNotification
|
@@ -0,0 +1,105 @@
|
|
1
|
+
"""
|
2
|
+
Tests for TestBrazePushNotificationChannel.
|
3
|
+
"""
|
4
|
+
from unittest.mock import MagicMock, patch
|
5
|
+
|
6
|
+
import pytest
|
7
|
+
|
8
|
+
from django.contrib.auth import get_user_model
|
9
|
+
from django.test import TestCase, override_settings
|
10
|
+
|
11
|
+
from edx_ace.channel.braze_push_notification import BrazePushNotificationChannel
|
12
|
+
from edx_ace.message import Message
|
13
|
+
from edx_ace.recipient import Recipient
|
14
|
+
from edx_ace.renderers import RenderedBrazePushNotification
|
15
|
+
|
16
|
+
BRAZE_URL = "https://example.braze.com"
|
17
|
+
API_KEY = "test-api-key"
|
18
|
+
User = get_user_model()
|
19
|
+
|
20
|
+
|
21
|
+
@pytest.mark.django_db
|
22
|
+
@override_settings(
|
23
|
+
EDX_BRAZE_API_KEY=API_KEY,
|
24
|
+
EDX_BRAZE_API_SERVER=BRAZE_URL,
|
25
|
+
)
|
26
|
+
class TestBrazePushNotificationChannel(TestCase):
|
27
|
+
|
28
|
+
def setUp(self):
|
29
|
+
super().setUp()
|
30
|
+
self.user = User.objects.create(username='username', email='email@example.com')
|
31
|
+
self.lms_user_id = self.user.id
|
32
|
+
self.mocked_post_data = {
|
33
|
+
'notification_type': 'new_response',
|
34
|
+
'course_id': 'course-v1:edX+DemoX+Demo_Course',
|
35
|
+
'content_url': 'http://localhost',
|
36
|
+
'replier_name': 'verified',
|
37
|
+
'post_title': 'New test response',
|
38
|
+
'course_name': 'Demonstration Course',
|
39
|
+
'thread_id': '67bedeb9ceb0b101343294c5',
|
40
|
+
'topic_id': 'i4x-edx-eiorguegnru-course-foobarbaz',
|
41
|
+
'response_id': '67ffa1f1ceb0b10134db3d8e',
|
42
|
+
'comment_id': None,
|
43
|
+
'strong': 'strong', 'p': 'p'
|
44
|
+
}
|
45
|
+
|
46
|
+
self.mocked_payload = {
|
47
|
+
'campaign_id': '1234test',
|
48
|
+
'trigger_properties': self.mocked_post_data,
|
49
|
+
'emails': ['edx@example.com']
|
50
|
+
}
|
51
|
+
|
52
|
+
@patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=True)
|
53
|
+
def test_enabled(self, mock_braze_client):
|
54
|
+
"""
|
55
|
+
Test that the channel is enabled when the settings are configured.
|
56
|
+
"""
|
57
|
+
assert BrazePushNotificationChannel.enabled()
|
58
|
+
|
59
|
+
@patch('edx_ace.channel.braze_push_notification.get_braze_client', return_value=False)
|
60
|
+
def test_disabled(self, mock_braze_client):
|
61
|
+
"""
|
62
|
+
Test that the channel is disabled when the settings are not configured.
|
63
|
+
"""
|
64
|
+
assert not BrazePushNotificationChannel.enabled()
|
65
|
+
|
66
|
+
@override_settings(ACE_CHANNEL_BRAZE_PUSH_CAMPAIGNS={'new_response': "1234test"})
|
67
|
+
@patch('edx_ace.channel.braze_push_notification.get_braze_client')
|
68
|
+
def test_deliver_success(self, mock_braze_function):
|
69
|
+
mock_braze_client = MagicMock()
|
70
|
+
mock_braze_function.return_value = mock_braze_client
|
71
|
+
mock_braze_client.send_campaign_message = MagicMock(return_value=True)
|
72
|
+
|
73
|
+
rendered_message = RenderedBrazePushNotification()
|
74
|
+
message = Message(
|
75
|
+
app_label='testapp',
|
76
|
+
name="test_braze",
|
77
|
+
recipient=Recipient(lms_user_id="1", email_address="user@example.com"),
|
78
|
+
context={'post_data': self.mocked_post_data},
|
79
|
+
options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'}
|
80
|
+
)
|
81
|
+
channel = BrazePushNotificationChannel()
|
82
|
+
channel.deliver(message, rendered_message)
|
83
|
+
mock_braze_client.send_campaign_message.assert_called_once()
|
84
|
+
args, kwargs = mock_braze_client.send_campaign_message.call_args
|
85
|
+
|
86
|
+
# Verify the payload
|
87
|
+
self.assertEqual(kwargs, self.mocked_payload)
|
88
|
+
|
89
|
+
@patch('edx_ace.channel.braze_push_notification.get_braze_client')
|
90
|
+
def test_campaign_not_configured(self, mock_braze_function):
|
91
|
+
mock_braze_client = MagicMock()
|
92
|
+
mock_braze_function.return_value = mock_braze_client
|
93
|
+
mock_braze_client.send_campaign_message = MagicMock(return_value=True)
|
94
|
+
|
95
|
+
rendered_message = RenderedBrazePushNotification()
|
96
|
+
message = Message(
|
97
|
+
app_label='testapp',
|
98
|
+
name="test_braze",
|
99
|
+
recipient=Recipient(lms_user_id="1", email_address="user@example.com"),
|
100
|
+
context={'post_data': self.mocked_post_data},
|
101
|
+
options={'emails': ['edx@example.com'], 'braze_campaign': 'new_response'}
|
102
|
+
)
|
103
|
+
channel = BrazePushNotificationChannel()
|
104
|
+
channel.deliver(message, rendered_message)
|
105
|
+
mock_braze_client.send_campaign_message.assert_not_called()
|
@@ -24,12 +24,16 @@ class TestPolicy(TestCase):
|
|
24
24
|
PolicyCase(deny_values=[set(ChannelType)], expected_channels=set()),
|
25
25
|
|
26
26
|
# deny only email
|
27
|
-
PolicyCase(deny_values=[{ChannelType.EMAIL}],
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
PolicyCase(deny_values=[{ChannelType.EMAIL}],
|
28
|
+
expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # single policy
|
29
|
+
PolicyCase(deny_values=[{ChannelType.EMAIL}, set()],
|
30
|
+
expected_channels={ChannelType.PUSH, ChannelType.BRAZE_PUSH}), # multiple policies
|
31
|
+
|
32
|
+
# deny email, push and braze_push
|
33
|
+
PolicyCase(deny_values=[{ChannelType.EMAIL, ChannelType.PUSH, ChannelType.BRAZE_PUSH}],
|
34
|
+
expected_channels=set()), # single policy
|
35
|
+
PolicyCase(deny_values=[{ChannelType.EMAIL}, {ChannelType.PUSH},
|
36
|
+
{ChannelType.BRAZE_PUSH}], expected_channels=set()), # multiple policies
|
33
37
|
|
34
38
|
# deny all and email
|
35
39
|
PolicyCase(deny_values=[{ChannelType.EMAIL}, set(ChannelType)], expected_channels=set()),
|
@@ -0,0 +1,59 @@
|
|
1
|
+
"""
|
2
|
+
Test cases for utils.braze
|
3
|
+
"""
|
4
|
+
from unittest.mock import patch
|
5
|
+
|
6
|
+
import pytest
|
7
|
+
|
8
|
+
from django.contrib.auth import get_user_model
|
9
|
+
from django.test import TestCase, override_settings
|
10
|
+
|
11
|
+
from edx_ace.utils.braze import get_braze_client
|
12
|
+
|
13
|
+
BRAZE_URL = "https://example.braze.com"
|
14
|
+
API_KEY = "test-api-key"
|
15
|
+
User = get_user_model()
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.mark.django_db
|
19
|
+
class TestBrazeClient(TestCase):
|
20
|
+
""" Test cases for utils.braze """
|
21
|
+
|
22
|
+
@patch('edx_ace.utils.braze.BrazeClient')
|
23
|
+
def test_disabled(self, mock_braze_client):
|
24
|
+
"""
|
25
|
+
Test that the channel is settings aren't configured.
|
26
|
+
"""
|
27
|
+
result = get_braze_client()
|
28
|
+
self.assertEqual(result, None)
|
29
|
+
mock_braze_client.assert_not_called()
|
30
|
+
|
31
|
+
@override_settings(ACE_CHANNEL_BRAZE_API_KEY=API_KEY)
|
32
|
+
@patch('edx_ace.utils.braze.BrazeClient')
|
33
|
+
def test_braze_url_not_configured(self, mock_braze_client):
|
34
|
+
"""
|
35
|
+
Test that the channel is settings aren't configured.
|
36
|
+
"""
|
37
|
+
result = get_braze_client()
|
38
|
+
self.assertEqual(result, None)
|
39
|
+
mock_braze_client.assert_not_called()
|
40
|
+
|
41
|
+
@override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY)
|
42
|
+
@patch('edx_ace.utils.braze.BrazeClient')
|
43
|
+
def test_braze_api_key_not_configured(self, mock_braze_client):
|
44
|
+
"""
|
45
|
+
Test that the channel is settings aren't configured.
|
46
|
+
"""
|
47
|
+
result = get_braze_client()
|
48
|
+
self.assertEqual(result, None)
|
49
|
+
mock_braze_client.assert_not_called()
|
50
|
+
|
51
|
+
@override_settings(ACE_CHANNEL_BRAZE_REST_ENDPOINT=API_KEY, ACE_CHANNEL_BRAZE_API_KEY=API_KEY)
|
52
|
+
@patch('edx_ace.utils.braze.BrazeClient', return_value=True)
|
53
|
+
def test_success(self, mock_braze_client):
|
54
|
+
"""
|
55
|
+
Test that the channel is settings aren't configured.
|
56
|
+
"""
|
57
|
+
result = get_braze_client()
|
58
|
+
self.assertEqual(result, True)
|
59
|
+
mock_braze_client.assert_called_once()
|
@@ -0,0 +1,27 @@
|
|
1
|
+
"""
|
2
|
+
Helper Methods related to braze client
|
3
|
+
"""
|
4
|
+
|
5
|
+
try:
|
6
|
+
from braze.client import BrazeClient
|
7
|
+
except ImportError:
|
8
|
+
BrazeClient = None
|
9
|
+
from django.conf import settings
|
10
|
+
|
11
|
+
|
12
|
+
def get_braze_client():
|
13
|
+
""" Returns a Braze client. """
|
14
|
+
if not BrazeClient:
|
15
|
+
return None
|
16
|
+
|
17
|
+
braze_api_key = getattr(settings, 'ACE_CHANNEL_BRAZE_API_KEY', None)
|
18
|
+
braze_api_url = getattr(settings, 'ACE_CHANNEL_BRAZE_REST_ENDPOINT', None)
|
19
|
+
|
20
|
+
if not braze_api_key or not braze_api_url:
|
21
|
+
return None
|
22
|
+
|
23
|
+
return BrazeClient(
|
24
|
+
api_key=braze_api_key,
|
25
|
+
api_url=f"https://{braze_api_url}",
|
26
|
+
app_id='',
|
27
|
+
)
|
@@ -27,6 +27,7 @@ edx_ace.egg-info/requires.txt
|
|
27
27
|
edx_ace.egg-info/top_level.txt
|
28
28
|
edx_ace/channel/__init__.py
|
29
29
|
edx_ace/channel/braze.py
|
30
|
+
edx_ace/channel/braze_push_notification.py
|
30
31
|
edx_ace/channel/django_email.py
|
31
32
|
edx_ace/channel/file.py
|
32
33
|
edx_ace/channel/mixins.py
|
@@ -42,6 +43,7 @@ edx_ace/tests/test_message.py
|
|
42
43
|
edx_ace/tests/test_policy.py
|
43
44
|
edx_ace/tests/test_presentation.py
|
44
45
|
edx_ace/tests/channel/test_braze.py
|
46
|
+
edx_ace/tests/channel/test_braze_push_notification.py
|
45
47
|
edx_ace/tests/channel/test_channel_helpers.py
|
46
48
|
edx_ace/tests/channel/test_django_email.py
|
47
49
|
edx_ace/tests/channel/test_file_email.py
|
@@ -49,8 +51,10 @@ edx_ace/tests/channel/test_push_notification.py
|
|
49
51
|
edx_ace/tests/channel/test_sailthru.py
|
50
52
|
edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/body.html
|
51
53
|
edx_ace/tests/test_templates/testapp/edx_ace/testmessage/email/head.html
|
54
|
+
edx_ace/tests/utils/test_braze_utils.py
|
52
55
|
edx_ace/tests/utils/test_signals.py
|
53
56
|
edx_ace/utils/__init__.py
|
57
|
+
edx_ace/utils/braze.py
|
54
58
|
edx_ace/utils/date.py
|
55
59
|
edx_ace/utils/once.py
|
56
60
|
edx_ace/utils/plugins.py
|
@@ -1,5 +1,6 @@
|
|
1
1
|
[openedx.ace.channel]
|
2
2
|
braze_email = edx_ace.channel.braze:BrazeEmailChannel
|
3
|
+
braze_push = edx_ace.channel.braze_push_notification:BrazePushNotificationChannel
|
3
4
|
django_email = edx_ace.channel.django_email:DjangoEmailChannel
|
4
5
|
file_email = edx_ace.channel.file:FileEmailChannel
|
5
6
|
push_notification = edx_ace.channel.push_notification:PushNotificationChannel
|
@@ -30,20 +30,48 @@ def load_requirements(*requirements_paths):
|
|
30
30
|
"""
|
31
31
|
# UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why.
|
32
32
|
|
33
|
+
# e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"}
|
34
|
+
by_canonical_name = {}
|
35
|
+
|
36
|
+
def check_name_consistent(package):
|
37
|
+
"""
|
38
|
+
Raise exception if package is named different ways.
|
39
|
+
|
40
|
+
This ensures that packages are named consistently so we can match
|
41
|
+
constraints to packages. It also ensures that if we require a package
|
42
|
+
with extras we don't constrain it without mentioning the extras (since
|
43
|
+
that too would interfere with matching constraints.)
|
44
|
+
"""
|
45
|
+
canonical = package.lower().replace('_', '-').split('[')[0]
|
46
|
+
seen_spelling = by_canonical_name.get(canonical)
|
47
|
+
if seen_spelling is None:
|
48
|
+
by_canonical_name[canonical] = package
|
49
|
+
elif seen_spelling != package:
|
50
|
+
raise Exception(
|
51
|
+
f'Encountered both "{seen_spelling}" and "{package}" in requirements '
|
52
|
+
'and constraints files; please use just one or the other.'
|
53
|
+
)
|
54
|
+
|
33
55
|
requirements = {}
|
34
56
|
constraint_files = set()
|
35
57
|
|
36
|
-
# groups "
|
37
|
-
|
58
|
+
# groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...")
|
59
|
+
re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name
|
60
|
+
# Two groups: name[maybe,extras], and optionally a constraint
|
61
|
+
requirement_line_regex = re.compile(
|
62
|
+
r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?"
|
63
|
+
% (re_package_name_base_chars, re_package_name_base_chars)
|
64
|
+
)
|
38
65
|
|
39
66
|
def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present):
|
40
67
|
regex_match = requirement_line_regex.match(current_line)
|
41
68
|
if regex_match:
|
42
69
|
package = regex_match.group(1)
|
43
70
|
version_constraints = regex_match.group(2)
|
71
|
+
check_name_consistent(package)
|
44
72
|
existing_version_constraints = current_requirements.get(package, None)
|
45
|
-
#
|
46
|
-
# constraints in place
|
73
|
+
# It's fine to add constraints to an unconstrained package,
|
74
|
+
# but raise an error if there are already constraints in place.
|
47
75
|
if existing_version_constraints and existing_version_constraints != version_constraints:
|
48
76
|
raise BaseException(f'Multiple constraint definitions found for {package}:'
|
49
77
|
f' "{existing_version_constraints}" and "{version_constraints}".'
|
@@ -52,7 +80,8 @@ def load_requirements(*requirements_paths):
|
|
52
80
|
if add_if_not_present or package in current_requirements:
|
53
81
|
current_requirements[package] = version_constraints
|
54
82
|
|
55
|
-
#
|
83
|
+
# Read requirements from .in files and store the path to any
|
84
|
+
# constraint files that are pulled in.
|
56
85
|
for path in requirements_paths:
|
57
86
|
with open(path) as reqs:
|
58
87
|
for line in reqs:
|
@@ -61,7 +90,7 @@ def load_requirements(*requirements_paths):
|
|
61
90
|
if line and line.startswith('-c') and not line.startswith('-c http'):
|
62
91
|
constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip())
|
63
92
|
|
64
|
-
# process constraint files
|
93
|
+
# process constraint files: add constraints to existing requirements
|
65
94
|
for constraint_file in constraint_files:
|
66
95
|
with open(constraint_file) as reader:
|
67
96
|
for line in reader:
|
@@ -136,6 +165,7 @@ setup(
|
|
136
165
|
'file_email = edx_ace.channel.file:FileEmailChannel',
|
137
166
|
'django_email = edx_ace.channel.django_email:DjangoEmailChannel',
|
138
167
|
'push_notification = edx_ace.channel.push_notification:PushNotificationChannel',
|
168
|
+
'braze_push = edx_ace.channel.braze_push_notification:BrazePushNotificationChannel',
|
139
169
|
]
|
140
170
|
}
|
141
171
|
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|