diff --git a/edx_ace/__init__.py b/edx_ace/__init__.py index 09590995..69bd5d5c 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.8.0' +__version__ = '1.9.0' __all__ = [ diff --git a/edx_ace/ace.py b/edx_ace/ace.py index 010cc1f9..a97531af 100644 --- a/edx_ace/ace.py +++ b/edx_ace/ace.py @@ -19,12 +19,14 @@ ) ace.send(msg) """ +from django.template import TemplateDoesNotExist + from edx_ace import delivery, policy, presentation from edx_ace.channel import get_channel_for_message from edx_ace.errors import ChannelError, UnsupportedChannelError -def send(msg): +def send(msg, limit_to_channels=None): """ Send a message to a recipient. @@ -37,12 +39,21 @@ def send(msg): Args: msg (Message): The message to send. + limit_to_channels (list of ChannelType, optional): If provided, only send the message over the specified + channels. If not provided, the message will be sent over all channels that the policies allow. """ msg.report_basics() channels_for_message = policy.channels_for(msg) for channel_type in channels_for_message: + if limit_to_channels and channel_type not in limit_to_channels: + msg.report( + 'channel_skipped', + f'Skipping channel {channel_type}' + ) + continue + try: channel = get_channel_for_message(channel_type, msg) except UnsupportedChannelError: @@ -50,6 +61,14 @@ def send(msg): try: rendered_message = presentation.render(channel, msg) + except TemplateDoesNotExist as error: + msg.report( + 'template_error', + str(error) + ) + continue + + try: delivery.deliver(channel, rendered_message, msg) except ChannelError as error: msg.report( diff --git a/edx_ace/channel/__init__.py b/edx_ace/channel/__init__.py index 87e62bd9..171003c6 100644 --- a/edx_ace/channel/__init__.py +++ b/edx_ace/channel/__init__.py @@ -78,6 +78,7 @@ class ChannelMap: """ A class that represents a channel map, usually as described in Django settings and `setup.py` files. """ + def __init__(self, channels_list): """ Initialize a ChannelMap. @@ -170,6 +171,7 @@ def get_channel_for_message(channel_type, message): Channel: The selected channel object. """ channels_map = channels() + channel_names = [] if channel_type == ChannelType.EMAIL: if message.options.get('transactional'): @@ -177,20 +179,21 @@ def get_channel_for_message(channel_type, message): else: channel_names = [settings.ACE_CHANNEL_DEFAULT_EMAIL] - try: - possible_channels = [ - channels_map.get_channel_by_name(channel_type, channel_name) - for channel_name in channel_names - ] - except KeyError: - return channels_map.get_default_channel(channel_type) - - # First see if any channel specifically demands to deliver this message - for channel in possible_channels: - if channel.overrides_delivery_for_message(message): - return channel - - # Else the normal path: use the preferred channel for this message type - return possible_channels[0] - - return channels_map.get_default_channel(channel_type) + if channel_type == ChannelType.PUSH: + channel_names = [settings.ACE_CHANNEL_DEFAULT_PUSH] + + try: + possible_channels = [ + channels_map.get_channel_by_name(channel_type, channel_name) + for channel_name in channel_names + ] + except KeyError: + return channels_map.get_default_channel(channel_type) + + # First see if any channel specifically demands to deliver this message + for channel in possible_channels: + if channel.overrides_delivery_for_message(message): + return channel + + # Else the normal path: use the preferred channel for this message type + return possible_channels[0] if possible_channels else channels_map.get_default_channel(channel_type) diff --git a/edx_ace/channel/push_notification.py b/edx_ace/channel/push_notification.py new file mode 100644 index 00000000..aec9142d --- /dev/null +++ b/edx_ace/channel/push_notification.py @@ -0,0 +1,107 @@ +""" +Channel for sending push notifications. +""" +import logging +import re + +from django.conf import settings + +from firebase_admin.messaging import Aps, ApsAlert, APNSConfig, APNSPayload +from push_notifications.gcm import send_message, dict_to_fcm_message +from push_notifications.models import GCMDevice + +from edx_ace.channel import Channel, ChannelType +from edx_ace.errors import FatalChannelDeliveryError +from edx_ace.message import Message +from edx_ace.renderers import RenderedPushNotification + +LOG = logging.getLogger(__name__) +APNS_DEFAULT_PRIORITY = '5' +APNS_DEFAULT_PUSH_TYPE = 'alert' + + +class PushNotificationChannel(Channel): + """ + A channel for sending push notifications. + """ + + channel_type = ChannelType.PUSH + + @classmethod + def enabled(cls): + """ + Returns true if the push notification settings are configured. + """ + return getattr(settings, 'PUSH_NOTIFICATIONS_SETTINGS', None) + + 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. + """ + device_tokens = self.get_user_device_tokens(message.recipient.lms_user_id) + if not device_tokens: + LOG.info(f'Recipient {message.recipient.email_address} has no push token. Skipping push notification.') + return + + for token in device_tokens: + self.send_message(message, token, rendered_message) + + def send_message(self, message: Message, token: str, rendered_message: RenderedPushNotification) -> None: + """ + Send a push notification to a device by token. + """ + notification_data = { + 'title': self.sanitize_html(rendered_message.subject), + 'body': self.sanitize_html(rendered_message.body), + 'notification_key': token, + **message.context.get('push_notification_extra_context', {}), + } + message = dict_to_fcm_message(notification_data) + # Note: By default dict_to_fcm_message does not support APNS configuration, + # only Android configuration, so we need to collect and set it manually. + apns_config = self.collect_apns_config(notification_data) + message.apns = apns_config + try: + send_message(token, message, settings.FCM_APP_NAME) + except Exception as e: + LOG.exception(f'Failed to send push notification to {token}') + raise FatalChannelDeliveryError(f'Failed to send push notification to {token}') + + @staticmethod + def collect_apns_config(notification_data: dict) -> APNSConfig: + """ + Collect APNS configuration with payload for the push notification. + + This APNSConfig must be set to notifications for Firebase to send push notifications to iOS devices. + Notification has default priority and visibility settings, described in Apple's documentation. + (https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns) + """ + apns_alert = ApsAlert(title=notification_data['title'], body=notification_data['body']) + aps = Aps(alert=apns_alert, sound='default') + return APNSConfig( + headers={'apns-priority': APNS_DEFAULT_PRIORITY, 'apns-push-type': APNS_DEFAULT_PUSH_TYPE}, + payload=APNSPayload(aps) + ) + + @staticmethod + def get_user_device_tokens(user_id: int) -> list: + """ + Get the device tokens for a user. + """ + return list(GCMDevice.objects.filter( + user_id=user_id, + cloud_message_type='FCM', + active=True, + ).values_list('registration_id', flat=True)) + + @staticmethod + def sanitize_html(html_str: str) -> str: + """ + Compress spaces and remove newlines to make it easier to author templates. + """ + return re.sub('\\s+', ' ', html_str, re.UNICODE).strip() diff --git a/edx_ace/presentation.py b/edx_ace/presentation.py index a70a08fa..cde5829a 100644 --- a/edx_ace/presentation.py +++ b/edx_ace/presentation.py @@ -9,6 +9,7 @@ RENDERERS = { ChannelType.EMAIL: renderers.EmailRenderer(), + ChannelType.PUSH: renderers.PushNotificationRenderer(), } diff --git a/edx_ace/push_notifications/views/__init__.py b/edx_ace/push_notifications/views/__init__.py new file mode 100644 index 00000000..6433e30e --- /dev/null +++ b/edx_ace/push_notifications/views/__init__.py @@ -0,0 +1 @@ +from push_notifications.api.rest_framework import GCMDeviceViewSet diff --git a/edx_ace/renderers.py b/edx_ace/renderers.py index 7ec7774b..a67e02c4 100644 --- a/edx_ace/renderers.py +++ b/edx_ace/renderers.py @@ -81,3 +81,21 @@ class EmailRenderer(AbstractRenderer): A renderer for :attr:`.ChannelType.EMAIL` channels. """ rendered_message_cls = RenderedEmail + + +@attr.s +class RenderedPushNotification: + """ + Encapsulates all values needed to send a :class:`.Message` + over an :attr:`.ChannelType.PUSH`. + """ + + subject = attr.ib() + body = attr.ib() + + +class PushNotificationRenderer(AbstractRenderer): + """ + A renderer for :attr:`.ChannelType.PUSH` channels. + """ + rendered_message_cls = RenderedPushNotification diff --git a/requirements/base.in b/requirements/base.in index e15b53b9..5d1959fe 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -7,3 +7,7 @@ attrs>=17.2.0 # Attributes without boilerplate sailthru-client==2.2.3 six stevedore>=1.10.0 + +firebase-admin==6.5.0 + +-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications diff --git a/requirements/ci.txt b/requirements/ci.txt index 127e7293..ed41257f 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade @@ -12,27 +12,23 @@ colorama==0.4.6 # via tox distlib==0.3.8 # via virtualenv -filelock==3.13.1 +filelock==3.14.0 # via # tox # virtualenv -packaging==23.2 +packaging==24.0 # via # pyproject-api # tox -platformdirs==4.2.0 +platformdirs==4.2.2 # via # tox # virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via tox pyproject-api==1.6.1 # via tox -tomli==2.0.1 - # via - # pyproject-api - # tox -tox==4.14.0 +tox==4.15.0 # via -r requirements/ci.in -virtualenv==20.25.1 +virtualenv==20.26.2 # via tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 3802d1b6..ebd9be0d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,29 +1,31 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -asgiref==3.7.2 +-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications + # via -r requirements/base.in +asgiref==3.8.1 # via django -astroid==3.1.0 +astroid==3.2.2 # via # pylint # pylint-celery attrs==23.2.0 # via -r requirements/base.in -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/dev.in - # -r requirements/quality.in - # django -build==1.1.1 +build==1.2.1 # via pip-tools +cachecontrol==0.14.0 + # via firebase-admin cachetools==5.3.3 - # via tox + # via + # google-auth + # tox certifi==2024.2.2 # via requests +cffi==1.16.0 + # via cryptography chardet==5.2.0 # via # diff-cover @@ -38,56 +40,113 @@ click==8.1.7 # pip-tools click-log==0.4.0 # via edx-lint -code-annotations==1.6.0 +code-annotations==1.8.0 # via edx-lint colorama==0.4.6 # via tox -diff-cover==8.0.3 +cryptography==42.0.7 + # via pyjwt +diff-cover==9.0.0 # via -r requirements/dev.in dill==0.3.8 # via pylint distlib==0.3.8 # via virtualenv -django==4.2.11 +django==4.2.13 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # django-push-notifications # edx-i18n-tools -docutils==0.20.1 +docutils==0.21.2 # via readme-renderer -edx-i18n-tools==1.3.0 +edx-i18n-tools==1.6.0 # via -r requirements/dev.in edx-lint==5.3.6 # via # -r requirements/dev.in # -r requirements/quality.in -filelock==3.13.1 +filelock==3.14.0 # via # tox # virtualenv -idna==3.6 +firebase-admin==6.5.0 + # via -r requirements/base.in +google-api-core[grpc]==2.19.0 + # via + # firebase-admin + # google-api-python-client + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-api-python-client==2.129.0 + # via firebase-admin +google-auth==2.29.0 + # via + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-cloud-core==2.4.1 + # via + # google-cloud-firestore + # google-cloud-storage +google-cloud-firestore==2.16.0 + # via firebase-admin +google-cloud-storage==2.16.0 + # via firebase-admin +google-crc32c==1.5.0 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via google-cloud-storage +googleapis-common-protos==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio-status==1.62.2 + # via google-api-core +httplib2==0.22.0 + # via + # google-api-python-client + # google-auth-httplib2 +idna==3.7 # via requests -importlib-metadata==7.0.1 +importlib-metadata==6.11.0 # via - # build - # keyring + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # twine -importlib-resources==6.1.2 - # via keyring isort==5.13.2 # via # -r requirements/quality.in # pylint -jaraco-classes==3.3.1 +jaraco-classes==3.4.0 + # via keyring +jaraco-context==5.3.0 # via keyring -jinja2==3.1.3 +jaraco-functools==4.0.1 + # via keyring +jinja2==3.1.4 # via # code-annotations # diff-cover -keyring==24.3.1 +keyring==25.2.1 # via twine -lxml==5.1.0 - # via edx-i18n-tools +lxml[html-clean,html_clean]==5.2.2 + # via + # edx-i18n-tools + # lxml-html-clean +lxml-html-clean==0.1.1 + # via lxml markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 @@ -97,15 +156,19 @@ mccabe==0.7.0 mdurl==0.1.2 # via markdown-it-py more-itertools==10.2.0 - # via jaraco-classes -nh3==0.2.15 + # via + # jaraco-classes + # jaraco-functools +msgpack==1.0.8 + # via cachecontrol +nh3==0.2.17 # via readme-renderer -packaging==23.2 +packaging==24.0 # via # build # pyproject-api # tox -path==16.10.0 +path==16.14.0 # via edx-i18n-tools pbr==6.0.0 # via stevedore @@ -113,27 +176,48 @@ pip-tools==7.4.1 # via -r requirements/dev.in pkginfo==1.10.0 # via twine -platformdirs==4.2.0 +platformdirs==4.2.2 # via # pylint # tox # virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via # diff-cover # tox polib==1.2.0 # via edx-i18n-tools +proto-plus==1.23.0 + # via + # google-api-core + # google-cloud-firestore +protobuf==4.25.3 + # via + # google-api-core + # google-cloud-firestore + # googleapis-common-protos + # grpcio-status + # proto-plus +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth pycodestyle==2.11.1 # via -r requirements/quality.in +pycparser==2.22 + # via cffi pydocstyle==6.3.0 # via -r requirements/quality.in -pygments==2.17.2 +pygments==2.18.0 # via # diff-cover # readme-renderer # rich -pylint==3.1.0 +pyjwt[crypto]==2.8.0 + # via firebase-admin +pylint==3.2.2 # via # edx-lint # pylint-celery @@ -147,9 +231,11 @@ pylint-plugin-utils==0.8.2 # via # pylint-celery # pylint-django +pyparsing==3.1.2 + # via httplib2 pyproject-api==1.6.1 # via tox -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via # build # pip-tools @@ -165,6 +251,9 @@ readme-renderer==43.0 # via twine requests==2.31.0 # via + # cachecontrol + # google-api-core + # google-cloud-storage # requests-toolbelt # sailthru-client # twine @@ -174,6 +263,8 @@ rfc3986==2.0.0 # via twine rich==13.7.1 # via twine +rsa==4.9 + # via google-auth sailthru-client==2.2.3 # via -r requirements/base.in simplejson==3.19.2 @@ -185,7 +276,7 @@ six==1.16.0 # python-dateutil snowballstemmer==2.2.0 # via pydocstyle -sqlparse==0.4.4 +sqlparse==0.5.0 # via django stevedore==5.2.0 # via @@ -193,40 +284,26 @@ stevedore==5.2.0 # code-annotations text-unidecode==1.3 # via python-slugify -tomli==2.0.1 - # via - # build - # pip-tools - # pylint - # pyproject-api - # pyproject-hooks - # tox -tomlkit==0.12.4 +tomlkit==0.12.5 # via pylint -tox==4.14.0 +tox==4.15.0 # via -r requirements/dev.in -twine==5.0.0 +twine==5.1.0 # via -r requirements/dev.in -typing-extensions==4.10.0 - # via - # asgiref - # astroid - # pylint - # rich +uritemplate==4.1.1 + # via google-api-python-client urllib3==2.2.1 # via # requests # twine -virtualenv==20.25.1 +virtualenv==20.26.2 # via tox -wheel==0.42.0 +wheel==0.43.0 # via # -r requirements/dev.in # pip-tools zipp==3.17.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/doc.txt b/requirements/doc.txt index c299e208..9089f194 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -1,61 +1,117 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -accessible-pygments==0.0.4 +-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications + # via -r requirements/base.in +accessible-pygments==0.0.5 # via pydata-sphinx-theme -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx -asgiref==3.7.2 +asgiref==3.8.1 # via django attrs==23.2.0 # via -r requirements/base.in -babel==2.14.0 +babel==2.15.0 # via # pydata-sphinx-theme # sphinx -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/doc.in - # django beautifulsoup4==4.12.3 # via pydata-sphinx-theme +cachecontrol==0.14.0 + # via firebase-admin +cachetools==5.3.3 + # via google-auth certifi==2024.2.2 # via requests +cffi==1.16.0 + # via cryptography charset-normalizer==3.3.2 # via requests -django==4.2.11 +cryptography==42.0.7 + # via pyjwt +django==4.2.13 # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in + # django-push-notifications doc8==1.1.1 # via -r requirements/doc.in -docutils==0.19 +docutils==0.20.1 # via # doc8 # pydata-sphinx-theme # readme-renderer # restructuredtext-lint # sphinx -idna==3.6 +firebase-admin==6.5.0 + # via -r requirements/base.in +google-api-core[grpc]==2.19.0 + # via + # firebase-admin + # google-api-python-client + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-api-python-client==2.129.0 + # via firebase-admin +google-auth==2.29.0 + # via + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-cloud-core==2.4.1 + # via + # google-cloud-firestore + # google-cloud-storage +google-cloud-firestore==2.16.0 + # via firebase-admin +google-cloud-storage==2.16.0 + # via firebase-admin +google-crc32c==1.5.0 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via google-cloud-storage +googleapis-common-protos==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio-status==1.62.2 + # via google-api-core +httplib2==0.22.0 + # via + # google-api-python-client + # google-auth-httplib2 +idna==3.7 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.0.1 +importlib-metadata==6.11.0 # via - # keyring - # sphinx + # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # twine -importlib-resources==6.1.2 +jaraco-classes==3.4.0 + # via keyring +jaraco-context==5.3.0 # via keyring -jaraco-classes==3.3.1 +jaraco-functools==4.0.1 # via keyring -jinja2==3.1.3 +jinja2==3.1.4 # via sphinx -keyring==24.3.1 +keyring==25.2.1 # via twine markdown-it-py==3.0.0 # via rich @@ -64,10 +120,14 @@ markupsafe==2.1.5 mdurl==0.1.2 # via markdown-it-py more-itertools==10.2.0 - # via jaraco-classes -nh3==0.2.15 + # via + # jaraco-classes + # jaraco-functools +msgpack==1.0.8 + # via cachecontrol +nh3==0.2.17 # via readme-renderer -packaging==23.2 +packaging==24.0 # via # pydata-sphinx-theme # sphinx @@ -75,9 +135,28 @@ pbr==6.0.0 # via stevedore pkginfo==1.10.0 # via twine -pydata-sphinx-theme==0.14.4 +proto-plus==1.23.0 + # via + # google-api-core + # google-cloud-firestore +protobuf==4.25.3 + # via + # google-api-core + # google-cloud-firestore + # googleapis-common-protos + # grpcio-status + # proto-plus +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth +pycparser==2.22 + # via cffi +pydata-sphinx-theme==0.15.2 # via sphinx-book-theme -pygments==2.17.2 +pygments==2.18.0 # via # accessible-pygments # doc8 @@ -85,16 +164,21 @@ pygments==2.17.2 # readme-renderer # rich # sphinx +pyjwt[crypto]==2.8.0 + # via firebase-admin +pyparsing==3.1.2 + # via httplib2 python-dateutil==2.9.0.post0 # via -r requirements/base.in -pytz==2024.1 - # via babel readme-renderer==43.0 # via # -r requirements/doc.in # twine requests==2.31.0 # via + # cachecontrol + # google-api-core + # google-cloud-storage # requests-toolbelt # sailthru-client # sphinx @@ -107,6 +191,8 @@ rfc3986==2.0.0 # via twine rich==13.7.1 # via twine +rsa==4.9 + # via google-auth sailthru-client==2.2.3 # via -r requirements/base.in simplejson==3.19.2 @@ -119,45 +205,40 @@ snowballstemmer==2.2.0 # via sphinx soupsieve==2.5 # via beautifulsoup4 -sphinx==6.2.1 +sphinx==7.3.7 # via # -r requirements/doc.in # pydata-sphinx-theme # sphinx-book-theme -sphinx-book-theme==1.0.1 +sphinx-book-theme==1.1.2 # via -r requirements/doc.in -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==1.0.8 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==1.0.6 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.0.5 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==1.0.7 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==1.1.10 # via sphinx -sqlparse==0.4.4 +sqlparse==0.5.0 # via django stevedore==5.2.0 # via # -r requirements/base.in # doc8 -tomli==2.0.1 - # via doc8 -twine==5.0.0 +twine==5.1.0 # via -r requirements/doc.in -typing-extensions==4.10.0 - # via - # asgiref - # pydata-sphinx-theme - # rich +typing-extensions==4.11.0 + # via pydata-sphinx-theme +uritemplate==4.1.1 + # via google-api-python-client urllib3==2.2.1 # via # requests # twine zipp==3.17.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt index 921c5b5c..0adf54ba 100644 --- a/requirements/pip-tools.txt +++ b/requirements/pip-tools.txt @@ -1,32 +1,23 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -build==1.1.1 +build==1.2.1 # via pip-tools click==8.1.7 # via pip-tools -importlib-metadata==7.0.1 - # via build -packaging==23.2 +packaging==24.0 # via build pip-tools==7.4.1 # via -r requirements/pip-tools.in -pyproject-hooks==1.0.0 - # via - # build - # pip-tools -tomli==2.0.1 +pyproject-hooks==1.1.0 # via # build # pip-tools - # pyproject-hooks -wheel==0.42.0 +wheel==0.43.0 # via pip-tools -zipp==3.17.0 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip diff --git a/requirements/pip.txt b/requirements/pip.txt index 66656035..8ed3e673 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1,14 +1,14 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -wheel==0.42.0 +wheel==0.43.0 # via -r requirements/pip.in # The following packages are considered to be unsafe in a requirements file: pip==24.0 # via -r requirements/pip.in -setuptools==69.1.1 +setuptools==69.5.1 # via -r requirements/pip.in diff --git a/requirements/quality.txt b/requirements/quality.txt index 30921d25..1b74fb2e 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -1,17 +1,13 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -astroid==3.1.0 +astroid==3.2.2 # via # pylint # pylint-celery -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/quality.in click==8.1.7 # via # click-log @@ -19,7 +15,7 @@ click==8.1.7 # edx-lint click-log==0.4.0 # via edx-lint -code-annotations==1.6.0 +code-annotations==1.8.0 # via edx-lint dill==0.3.8 # via pylint @@ -29,7 +25,7 @@ isort==5.13.2 # via # -r requirements/quality.in # pylint -jinja2==3.1.3 +jinja2==3.1.4 # via code-annotations markupsafe==2.1.5 # via jinja2 @@ -37,13 +33,13 @@ mccabe==0.7.0 # via pylint pbr==6.0.0 # via stevedore -platformdirs==4.2.0 +platformdirs==4.2.2 # via pylint pycodestyle==2.11.1 # via -r requirements/quality.in pydocstyle==6.3.0 # via -r requirements/quality.in -pylint==3.1.0 +pylint==3.2.2 # via # edx-lint # pylint-celery @@ -69,11 +65,5 @@ stevedore==5.2.0 # via code-annotations text-unidecode==1.3 # via python-slugify -tomli==2.0.1 - # via pylint -tomlkit==0.12.4 +tomlkit==0.12.5 # via pylint -typing-extensions==4.10.0 - # via - # astroid - # pylint diff --git a/requirements/test.txt b/requirements/test.txt index c5d4b131..df92650d 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,72 +1,146 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # make upgrade # -asgiref==3.7.2 +-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications + # via -r requirements/base.in +asgiref==3.8.1 # via django attrs==23.2.0 # via # -r requirements/base.in # hypothesis -backports-zoneinfo==0.2.1 ; python_version < "3.9" - # via - # -c requirements/constraints.txt - # -r requirements/test.in - # django +cachecontrol==0.14.0 + # via firebase-admin +cachetools==5.3.3 + # via google-auth certifi==2024.2.2 # via requests +cffi==1.16.0 + # via cryptography charset-normalizer==3.3.2 # via requests -coverage[toml]==7.4.3 +coverage[toml]==7.5.1 # via pytest-cov +cryptography==42.0.7 + # via pyjwt ddt==1.7.2 # via -r requirements/test.in # via # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt # -r requirements/base.in -exceptiongroup==1.2.0 + # django-push-notifications +firebase-admin==6.5.0 + # via -r requirements/base.in +google-api-core[grpc]==2.19.0 # via - # hypothesis - # pytest -hypothesis[pytz]==6.98.17 + # firebase-admin + # google-api-python-client + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-api-python-client==2.129.0 + # via firebase-admin +google-auth==2.29.0 + # via + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-cloud-core + # google-cloud-firestore + # google-cloud-storage +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-cloud-core==2.4.1 + # via + # google-cloud-firestore + # google-cloud-storage +google-cloud-firestore==2.16.0 + # via firebase-admin +google-cloud-storage==2.16.0 + # via firebase-admin +google-crc32c==1.5.0 + # via + # google-cloud-storage + # google-resumable-media +google-resumable-media==2.7.0 + # via google-cloud-storage +googleapis-common-protos==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio==1.63.0 + # via + # google-api-core + # grpcio-status +grpcio-status==1.62.2 + # via google-api-core +httplib2==0.22.0 + # via + # google-api-python-client + # google-auth-httplib2 +hypothesis[pytz]==6.102.4 # via # -r requirements/test.in # hypothesis-pytest hypothesis-pytest==0.19.0 # via -r requirements/test.in -idna==3.6 +idna==3.7 # via requests -importlib-metadata==7.0.1 - # via pytest-randomly iniconfig==2.0.0 # via pytest jedi==0.19.1 # via pudb mock==5.1.0 # via -r requirements/test.in -packaging==23.2 +msgpack==1.0.8 + # via cachecontrol +packaging==24.0 # via # pudb # pytest -parso==0.8.3 +parso==0.8.4 # via jedi pbr==6.0.0 # via stevedore -pluggy==1.4.0 +pluggy==1.5.0 # via pytest +proto-plus==1.23.0 + # via + # google-api-core + # google-cloud-firestore +protobuf==4.25.3 + # via + # google-api-core + # google-cloud-firestore + # googleapis-common-protos + # grpcio-status + # proto-plus pudb==2024.1 # via -r requirements/test.in -pygments==2.17.2 +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth +pycparser==2.22 + # via cffi +pygments==2.18.0 # via pudb -pytest==8.0.2 +pyjwt[crypto]==2.8.0 + # via firebase-admin +pyparsing==3.1.2 + # via httplib2 +pytest==8.2.1 # via # hypothesis-pytest # pytest-cov # pytest-django # pytest-randomly -pytest-cov==4.1.0 +pytest-cov==5.0.0 # via -r requirements/test.in pytest-django==4.8.0 # via -r requirements/test.in @@ -77,7 +151,13 @@ python-dateutil==2.9.0.post0 pytz==2024.1 # via hypothesis requests==2.31.0 - # via sailthru-client + # via + # cachecontrol + # google-api-core + # google-cloud-storage + # sailthru-client +rsa==4.9 + # via google-auth sailthru-client==2.2.3 # via -r requirements/base.in simplejson==3.19.2 @@ -88,21 +168,17 @@ six==1.16.0 # python-dateutil sortedcontainers==2.4.0 # via hypothesis -sqlparse==0.4.4 +sqlparse==0.5.0 # via django stevedore==5.2.0 # via -r requirements/base.in -tomli==2.0.1 - # via - # coverage - # pytest -typing-extensions==4.10.0 - # via - # asgiref - # urwid +typing-extensions==4.11.0 + # via urwid +uritemplate==4.1.1 + # via google-api-python-client urllib3==2.2.1 # via requests -urwid==2.6.8 +urwid==2.6.12 # via # pudb # urwid-readline @@ -110,5 +186,3 @@ urwid-readline==0.14 # via pudb wcwidth==0.2.13 # via urwid -zipp==3.17.0 - # via importlib-metadata diff --git a/setup.py b/setup.py index d5cc3239..6d4ed5c0 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,8 @@ def is_requirement(line): include_package_data=True, install_requires=load_requirements('requirements/base.in'), extras_require={ - 'sailthru': ["sailthru-client>2.2,<2.3"] + 'sailthru': ["sailthru-client>2.2,<2.3"], + 'push_notifications': ["django-push-notifications[FCM]>3.0.2"] }, license="AGPL 3.0", zip_safe=False, @@ -134,6 +135,7 @@ def is_requirement(line): 'sailthru_email = edx_ace.channel.sailthru:SailthruEmailChannel', 'file_email = edx_ace.channel.file:FileEmailChannel', 'django_email = edx_ace.channel.django_email:DjangoEmailChannel', + 'push_notification = edx_ace.channel.push_notification:PushNotificationChannel', ] } )