Skip to content

Commit

Permalink
feat: [AXM-252] add settings for edx-ace push notifications (#2541)
Browse files Browse the repository at this point in the history
* feat: [AXM-252] create policy for push notifications

* feat: [AXM-252] add API for store device token

* feat: [AXM-252] add settings for edx-ace push notifications

* chore: [AXM-252] add edx-ace and django-push-notification to dev requirements

* chore: [AXM-252] update edx-ace version

* fix: [AXM-252] add create token edndpoint to urls

* chore: [AXM-252] update django push notifications version

* style: [AXM-252] fix code style issues after review

* chore: [AXM-252] bump edx-ace version

* refactor: [AXM-252] some push notif policy refactoring

* chore: [AXM-252] change edx-ace branch to mob-develop

* chore: [AXM-252] recompile requirements after rebase
  • Loading branch information
NiedielnitsevIvan committed May 27, 2024
1 parent cdfa6fa commit 97392f5
Show file tree
Hide file tree
Showing 13 changed files with 626 additions and 13 deletions.
10 changes: 10 additions & 0 deletions lms/djangoapps/mobile_api/notifications/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path
from .views import GCMDeviceViewSet


CREATE_GCM_DEVICE = GCMDeviceViewSet.as_view({'post': 'create'})


urlpatterns = [
path('create-token/', CREATE_GCM_DEVICE, name='gcmdevice-list'),
]
50 changes: 50 additions & 0 deletions lms/djangoapps/mobile_api/notifications/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.conf import settings
from rest_framework import status
from rest_framework.response import Response

from edx_ace.push_notifications.views import GCMDeviceViewSet as GCMDeviceViewSetBase

from ..decorators import mobile_view


@mobile_view(is_user=True)
class GCMDeviceViewSet(GCMDeviceViewSetBase):
"""
**Use Case**
This endpoint allows clients to register a device for push notifications.
If the device is already registered, the existing registration will be updated.
If setting PUSH_NOTIFICATIONS_SETTINGS is not configured, the endpoint will return a 501 error.
**Example Request**
POST /api/mobile/{version}/notifications/create-token/
**POST Parameters**
The body of the POST request can include the following parameters.
* name (optional) - A name of the device.
* registration_id (required) - The device token of the device.
* device_id (optional) - ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)
* active (optional) - Whether the device is active, default is True.
If False, the device will not receive notifications.
* cloud_message_type (required) - You should choose FCM or GCM. Currently, only FCM is supported.
* application_id (optional) - Opaque application identity, should be filled in for multiple
key/certificate access.
**Example Response**
```json
{
"id": 1,
"name": "My Device",
"registration_id": "fj3j4",
"device_id": 1234,
"active": true,
"date_created": "2024-04-18T07:39:37.132787Z",
"cloud_message_type": "FCM",
"application_id": "my_app_id"
}
```
"""

def create(self, request, *args, **kwargs):
if not getattr(settings, 'PUSH_NOTIFICATIONS_SETTINGS', None):
return Response('Push notifications are not configured.', status.HTTP_501_NOT_IMPLEMENTED)

return super().create(request, *args, **kwargs)
1 change: 1 addition & 0 deletions lms/djangoapps/mobile_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
urlpatterns = [
path('users/', include('lms.djangoapps.mobile_api.users.urls')),
path('my_user_info', my_user_info, name='user-info'),
path('notifications/', include('lms.djangoapps.mobile_api.notifications.urls')),
path('course_info/', include('lms.djangoapps.mobile_api.course_info.urls')),
]
29 changes: 29 additions & 0 deletions openedx/core/djangoapps/ace_common/settings/common.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""
Settings for ace_common app.
"""
from openedx.core.djangoapps.ace_common.utils import setup_firebase_app

ACE_ROUTING_KEY = 'edx.lms.core.default'


def plugin_settings(settings): # lint-amnesty, pylint: disable=missing-function-docstring, missing-module-docstring
if 'push_notifications' not in settings.INSTALLED_APPS:
settings.INSTALLED_APPS.append('push_notifications')
settings.ACE_ENABLED_CHANNELS = [
'django_email'
]
Expand All @@ -22,3 +25,29 @@ def plugin_settings(settings): # lint-amnesty, pylint: disable=missing-function
settings.ACE_ROUTING_KEY = ACE_ROUTING_KEY

settings.FEATURES['test_django_plugin'] = True
settings.FCM_APP_NAME = 'fcm-edx-platform'

if getattr(settings, 'FIREBASE_SETUP_STATUS', None) is None:
settings.ACE_CHANNEL_DEFAULT_PUSH = 'push_notification'

# Note: To local development with Firebase, you must set FIREBASE_CREDENTIALS.
settings.FCM_APP_NAME = 'fcm-edx-platform'
settings.FIREBASE_CREDENTIALS = None

if firebase_app := setup_firebase_app(settings.FIREBASE_CREDENTIALS, settings.FCM_APP_NAME):
settings.ACE_ENABLED_CHANNELS.append(settings.ACE_CHANNEL_DEFAULT_PUSH)
settings.ACE_ENABLED_POLICIES.append(settings.ACE_CHANNEL_DEFAULT_PUSH)

settings.PUSH_NOTIFICATIONS_SETTINGS = {
'CONFIG': 'push_notifications.conf.AppConfig',
'APPLICATIONS': {
settings.FCM_APP_NAME: {
'PLATFORM': 'FCM',
'FIREBASE_APP': firebase_app,
},
},
'UPDATE_ON_DUPLICATE_REG_ID': True,
}
settings.FIREBASE_SETUP_STATUS = True
else:
settings.FIREBASE_SETUP_STATUS = False
22 changes: 22 additions & 0 deletions openedx/core/djangoapps/ace_common/settings/production.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Common environment variables unique to the ace_common plugin."""
from openedx.core.djangoapps.ace_common.utils import setup_firebase_app


def plugin_settings(settings):
Expand Down Expand Up @@ -26,3 +27,24 @@ def plugin_settings(settings):
settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL = settings.ENV_TOKENS.get(
'ACE_CHANNEL_TRANSACTIONAL_EMAIL', settings.ACE_CHANNEL_TRANSACTIONAL_EMAIL
)
settings.FCM_APP_NAME = settings.ENV_TOKENS.get('FCM_APP_NAME', 'fcm-edx-platform')
settings.FIREBASE_CREDENTIALS = settings.ENV_TOKENS.get('FIREBASE_CREDENTIALS', {})

if getattr(settings, 'FIREBASE_SETUP_STATUS', None) is None:
if firebase_app := setup_firebase_app(settings.FIREBASE_CREDENTIALS, settings.FCM_APP_NAME):
settings.ACE_ENABLED_CHANNELS.append(settings.ACE_CHANNEL_DEFAULT_PUSH)
settings.ACE_ENABLED_POLICIES.append(settings.ACE_CHANNEL_DEFAULT_PUSH)

settings.PUSH_NOTIFICATIONS_SETTINGS = {
'CONFIG': 'push_notifications.conf.AppConfig',
'APPLICATIONS': {
settings.FCM_APP_NAME: {
'PLATFORM': 'FCM',
'FIREBASE_APP': firebase_app,
},
},
'UPDATE_ON_DUPLICATE_REG_ID': True,
}
settings.FIREBASE_SETUP_STATUS = True
else:
settings.FIREBASE_SETUP_STATUS = False
20 changes: 20 additions & 0 deletions openedx/core/djangoapps/ace_common/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Utility functions for edx-ace.
"""
import logging

log = logging.getLogger(__name__)


def setup_firebase_app(firebase_credentials, app_name='fcm-app'):
"""
Returns a Firebase app instance if the Firebase credentials are provided.
"""
try:
import firebase_admin # pylint: disable=import-outside-toplevel
except ImportError:
log.error('Could not import firebase_admin package.')
return
if firebase_credentials:
certificate = firebase_admin.credentials.Certificate(firebase_credentials)
return firebase_admin.initialize_app(certificate, name=app_name)
41 changes: 41 additions & 0 deletions openedx/core/djangoapps/notifications/policies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Policies for the notifications app."""

from edx_ace.channel import ChannelType
from edx_ace.policy import Policy, PolicyResult
from opaque_keys.edx.keys import CourseKey

from .models import CourseNotificationPreference


class CoursePushNotificationOptout(Policy):
"""
Course Push Notification optOut Policy.
"""

def check(self, message):
"""
Check if the user has opted out of push notifications for the given course.
:param message:
:return:
"""
course_ids = message.context.get('course_ids', [])
app_label = message.context.get('app_label')

if not (app_label or message.context.get('send_push_notification', False)):
return PolicyResult(deny={ChannelType.PUSH})

course_keys = [CourseKey.from_string(course_id) for course_id in course_ids]
for course_key in course_keys:
course_notification_preference = CourseNotificationPreference.get_user_course_preference(
message.recipient.lms_user_id,
course_key
)
push_notification_preference = course_notification_preference.get_notification_type_config(
app_label,
notification_type='push',
).get('push', False)

if not push_notification_preference:
return PolicyResult(deny={ChannelType.PUSH})

return PolicyResult(deny=frozenset())
87 changes: 84 additions & 3 deletions requirements/edx/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
#
# make upgrade
#
-e git+https://github.com/jazzband/django-push-notifications.git@906fe52058bad36b6af2bb292fdb9292ccaa94e5#egg=django_push_notifications
# via -r requirements/edx/github.in
-e git+https://github.com/raccoongang/edx-ace.git@mob-develop#egg=edx_ace
# via
# -r requirements/edx/github.in
# -r requirements/edx/kernel.in
-e git+https://github.com/anupdhabarde/edx-proctoring-proctortrack.git@31c6c9923a51c903ae83760ecbbac191363aa2a2#egg=edx_proctoring_proctortrack
# via -r requirements/edx/github.in
acid-xblock==0.3.1
Expand Down Expand Up @@ -90,6 +96,10 @@ botocore==1.34.104
# s3transfer
bridgekeeper==0.9
# via -r requirements/edx/kernel.in
cachecontrol==0.14.0
# via firebase-admin
cachetools==5.3.3
# via google-auth
camel-converter[pydantic]==3.1.2
# via meilisearch
celery==5.4.0
Expand Down Expand Up @@ -195,6 +205,7 @@ django==4.2.13
# django-multi-email-field
# django-mysql
# django-oauth-toolkit
# django-push-notifications
# django-sekizai
# django-ses
# django-statici18n
Expand Down Expand Up @@ -400,8 +411,6 @@ drf-yasg==1.21.5
# -c requirements/edx/../constraints.txt
# django-user-tasks
# edx-api-doc-tools
edx-ace==1.8.0
# via -r requirements/edx/kernel.in
edx-api-doc-tools==1.8.0
# via
# -r requirements/edx/kernel.in
Expand Down Expand Up @@ -558,6 +567,8 @@ fastavro==1.9.4
# via openedx-events
filelock==3.14.0
# via snowflake-connector-python
firebase-admin==5.0.0
# via edx-ace
frozenlist==1.4.1
# via
# aiohttp
Expand All @@ -578,6 +589,49 @@ geoip2==4.8.0
# via -r requirements/edx/kernel.in
glob2==0.7
# via -r requirements/edx/kernel.in
google-api-core[grpc]==1.34.1
# via
# firebase-admin
# google-api-python-client
# google-cloud-core
# google-cloud-firestore
# google-cloud-storage
google-api-python-client==2.127.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.14.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.62.2
# via
# google-api-core
# grpcio-status
grpcio-status==1.48.2
# via google-api-core
gunicorn==22.0.0
# via -r requirements/edx/kernel.in
help-tokens==2.4.0
Expand All @@ -586,6 +640,10 @@ html5lib==1.1
# via
# -r requirements/edx/kernel.in
# ora2
httplib2==0.22.0
# via
# google-api-python-client
# google-auth-httplib2
icalendar==5.0.12
# via -r requirements/edx/kernel.in
idna==3.7
Expand Down Expand Up @@ -718,6 +776,8 @@ monotonic==1.6
# py2neo
mpmath==1.3.0
# via sympy
msgpack==1.0.8
# via cachecontrol
multidict==6.0.5
# via
# aiohttp
Expand Down Expand Up @@ -831,6 +891,15 @@ polib==1.2.0
# via edx-i18n-tools
prompt-toolkit==3.0.43
# via click-repl
proto-plus==1.23.0
# via google-cloud-firestore
protobuf==3.20.3
# via
# google-api-core
# google-cloud-firestore
# googleapis-common-protos
# grpcio-status
# proto-plus
psutil==5.9.8
# via
# -r requirements/edx/paver.txt
Expand All @@ -840,7 +909,12 @@ py2neo @ https://github.com/overhangio/py2neo/releases/download/2021.2.3/py2neo-
# -c requirements/edx/../constraints.txt
# -r requirements/edx/bundled.in
pyasn1==0.6.0
# via pgpy
# via
# pgpy
# pyasn1-modules
# rsa
pyasn1-modules==0.4.0
# via google-auth
pycountry==23.12.11
# via -r requirements/edx/kernel.in
pycparser==2.22
Expand Down Expand Up @@ -901,6 +975,7 @@ pyopenssl==24.1.0
pyparsing==3.1.2
# via
# chem
# httplib2
# openedx-calc
pyrsistent==0.20.0
# via optimizely-sdk
Expand Down Expand Up @@ -984,13 +1059,16 @@ requests==2.31.0
# -r requirements/edx/paver.txt
# algoliasearch
# analytics-python
# cachecontrol
# coreapi
# django-oauth-toolkit
# edx-bulk-grades
# edx-drf-extensions
# edx-enterprise
# edx-rest-api-client
# geoip2
# google-api-core
# google-cloud-storage
# mailsnake
# meilisearch
# openai
Expand All @@ -1012,6 +1090,8 @@ rpds-py==0.18.1
# via
# jsonschema
# referencing
rsa==4.9
# via google-auth
ruamel-yaml==0.18.6
# via drf-yasg
ruamel-yaml-clib==0.2.8
Expand Down Expand Up @@ -1154,6 +1234,7 @@ uritemplate==4.1.1
# coreapi
# drf-spectacular
# drf-yasg
# google-api-python-client
urllib3==1.26.18
# via
# -c requirements/edx/../constraints.txt
Expand Down
Loading

0 comments on commit 97392f5

Please sign in to comment.