feat: [FC-0047] add settings for edx-ace push notifications
feat: [FC-0047] Add push notifications for user enroll feat: [FC-0047] Add push notifications for user unenroll feat: [FC-0047] Add push notifications for add course beta testers feat: [FC-0047] Add push notifications for remove course beta testers feat: [FC-0047] Add push notification event to discussions
This commit is contained in:
committed by
Іван Нєдєльніцев
parent
f8bf592483
commit
28eb406f8d
@@ -109,8 +109,10 @@ def create_message_context(comment, site):
|
||||
'course_id': str(thread.course_id),
|
||||
'comment_id': comment.id,
|
||||
'comment_body': comment.body,
|
||||
'comment_body_text': comment.body_text,
|
||||
'comment_author_id': comment.user_id,
|
||||
'comment_created_at': comment.created_at, # comment_client models dates are already serialized
|
||||
'comment_parent_id': comment.parent_id,
|
||||
'thread_id': thread.id,
|
||||
'thread_title': thread.title,
|
||||
'thread_author_id': thread.user_id,
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.conf import settings # lint-amnesty, pylint: disable=unused-import
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.contrib.sites.models import Site
|
||||
from edx_ace import ace
|
||||
from edx_ace.channel import ChannelType
|
||||
from edx_ace.recipient import Recipient
|
||||
from edx_ace.utils import date
|
||||
from edx_django_utils.monitoring import set_code_owner_attribute
|
||||
@@ -74,6 +75,12 @@ class ReportedContentNotification(BaseMessageType):
|
||||
self.options['transactional'] = True
|
||||
|
||||
|
||||
class CommentNotification(BaseMessageType):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.options['transactional'] = True
|
||||
|
||||
|
||||
@shared_task(base=LoggedTask)
|
||||
@set_code_owner_attribute
|
||||
def send_ace_message(context): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
@@ -82,17 +89,40 @@ def send_ace_message(context): # lint-amnesty, pylint: disable=missing-function
|
||||
if _should_send_message(context):
|
||||
context['site'] = Site.objects.get(id=context['site_id'])
|
||||
thread_author = User.objects.get(id=context['thread_author_id'])
|
||||
with emulate_http_request(site=context['site'], user=thread_author):
|
||||
message_context = _build_message_context(context)
|
||||
comment_author = User.objects.get(id=context['comment_author_id'])
|
||||
with emulate_http_request(site=context['site'], user=comment_author):
|
||||
message_context = _build_message_context(context, notification_type='forum_response')
|
||||
message = ResponseNotification().personalize(
|
||||
Recipient(thread_author.id, thread_author.email),
|
||||
_get_course_language(context['course_id']),
|
||||
message_context
|
||||
)
|
||||
log.info('Sending forum comment email notification with context %s', message_context)
|
||||
ace.send(message)
|
||||
log.info('Sending forum comment notification with context %s', message_context)
|
||||
if _is_first_comment(context['comment_id'], context['thread_id']):
|
||||
limit_to_channels = None
|
||||
else:
|
||||
limit_to_channels = [ChannelType.PUSH]
|
||||
ace.send(message, limit_to_channels=limit_to_channels)
|
||||
_track_notification_sent(message, context)
|
||||
|
||||
elif _should_send_subcomment_message(context):
|
||||
context['site'] = Site.objects.get(id=context['site_id'])
|
||||
comment_author = User.objects.get(id=context['comment_author_id'])
|
||||
thread_author = User.objects.get(id=context['thread_author_id'])
|
||||
|
||||
with emulate_http_request(site=context['site'], user=comment_author):
|
||||
message_context = _build_message_context(context)
|
||||
message = CommentNotification().personalize(
|
||||
Recipient(thread_author.id, thread_author.email),
|
||||
_get_course_language(context['course_id']),
|
||||
message_context
|
||||
)
|
||||
log.info('Sending forum comment notification with context %s', message_context)
|
||||
ace.send(message, limit_to_channels=[ChannelType.PUSH])
|
||||
_track_notification_sent(message, context)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
@shared_task(base=LoggedTask)
|
||||
@set_code_owner_attribute
|
||||
@@ -154,19 +184,36 @@ def _should_send_message(context):
|
||||
return (
|
||||
_is_user_subscribed_to_thread(cc_thread_author, context['thread_id']) and
|
||||
_is_not_subcomment(context['comment_id']) and
|
||||
_is_first_comment(context['comment_id'], context['thread_id'])
|
||||
not _comment_author_is_thread_author(context)
|
||||
)
|
||||
|
||||
|
||||
def _should_send_subcomment_message(context):
|
||||
cc_thread_author = cc.User(id=context['thread_author_id'], course_id=context['course_id'])
|
||||
return (
|
||||
_is_user_subscribed_to_thread(cc_thread_author, context['thread_id']) and
|
||||
_is_subcomment(context['comment_id']) and
|
||||
not _comment_author_is_thread_author(context)
|
||||
)
|
||||
|
||||
|
||||
def _comment_author_is_thread_author(context):
|
||||
return context.get('comment_author_id', '') == context['thread_author_id']
|
||||
|
||||
|
||||
def _is_content_still_reported(context):
|
||||
if context.get('comment_id') is not None:
|
||||
return len(cc.Comment.find(context['comment_id']).abuse_flaggers) > 0
|
||||
return len(cc.Thread.find(context['thread_id']).abuse_flaggers) > 0
|
||||
|
||||
|
||||
def _is_not_subcomment(comment_id):
|
||||
def _is_subcomment(comment_id):
|
||||
comment = cc.Comment.find(id=comment_id).retrieve()
|
||||
return not getattr(comment, 'parent_id', None)
|
||||
return getattr(comment, 'parent_id', None)
|
||||
|
||||
|
||||
def _is_not_subcomment(comment_id):
|
||||
return not _is_subcomment(comment_id)
|
||||
|
||||
|
||||
def _is_first_comment(comment_id, thread_id): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
@@ -204,7 +251,7 @@ def _get_course_language(course_id):
|
||||
return language
|
||||
|
||||
|
||||
def _build_message_context(context): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
def _build_message_context(context, notification_type='forum_comment'): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
message_context = get_base_template_context(context['site'])
|
||||
message_context.update(context)
|
||||
thread_author = User.objects.get(id=context['thread_author_id'])
|
||||
@@ -218,6 +265,14 @@ def _build_message_context(context): # lint-amnesty, pylint: disable=missing-fu
|
||||
'thread_username': thread_author.username,
|
||||
'comment_username': comment_author.username,
|
||||
'post_link': post_link,
|
||||
'push_notification_extra_context': {
|
||||
'course_id': str(context['course_id']),
|
||||
'parent_id': str(context['comment_parent_id']),
|
||||
'notification_type': notification_type,
|
||||
'topic_id': str(context['thread_commentable_id']),
|
||||
'thread_id': context['thread_id'],
|
||||
'comment_id': context['comment_id'],
|
||||
},
|
||||
'comment_created_at': date.deserialize(context['comment_created_at']),
|
||||
'thread_created_at': date.deserialize(context['thread_created_at'])
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans trimmed %}{{ comment_username }} commented to {{ thread_title }}:{% endblocktrans %}
|
||||
{{ comment_body_text }}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% blocktrans %}Comment to {{ thread_title }}{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}{% endblocktrans %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans %}Response to {{ thread_title }}{% endblocktrans %}
|
||||
@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from lms.djangoapps.discussion.signals.handlers import ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY
|
||||
from lms.djangoapps.discussion.tasks import _should_send_message, _track_notification_sent
|
||||
from lms.djangoapps.discussion.tasks import _is_first_comment, _should_send_message, _track_notification_sent
|
||||
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangoapps.django_comment_common.models import ForumsConfig
|
||||
@@ -222,6 +222,8 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
|
||||
|
||||
self.ace_send_patcher = mock.patch('edx_ace.ace.send')
|
||||
self.mock_ace_send = self.ace_send_patcher.start()
|
||||
self.mock_message_patcher = mock.patch('lms.djangoapps.discussion.tasks.ResponseNotification')
|
||||
self.mock_message = self.mock_message_patcher.start()
|
||||
|
||||
thread_permalink = '/courses/discussion/dummy_discussion_id'
|
||||
self.permalink_patcher = mock.patch('lms.djangoapps.discussion.tasks.permalink', return_value=thread_permalink)
|
||||
@@ -231,10 +233,12 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
|
||||
super().tearDown()
|
||||
self.request_patcher.stop()
|
||||
self.ace_send_patcher.stop()
|
||||
self.mock_message_patcher.stop()
|
||||
self.permalink_patcher.stop()
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_send_discussion_email_notification(self, user_subscribed):
|
||||
self.mock_message_patcher.stop()
|
||||
if user_subscribed:
|
||||
non_matching_id = 'not-a-match'
|
||||
# with per_page left with a default value of 1, this ensures
|
||||
@@ -271,8 +275,10 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
|
||||
expected_message_context.update({
|
||||
'comment_author_id': self.comment_author.id,
|
||||
'comment_body': comment['body'],
|
||||
'comment_body_text': comment.body_text,
|
||||
'comment_created_at': ONE_HOUR_AGO,
|
||||
'comment_id': comment['id'],
|
||||
'comment_parent_id': comment['parent_id'],
|
||||
'comment_username': self.comment_author.username,
|
||||
'course_id': self.course.id,
|
||||
'thread_author_id': self.thread_author.id,
|
||||
@@ -283,7 +289,15 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
|
||||
'thread_commentable_id': thread['commentable_id'],
|
||||
'post_link': f'https://{site.domain}{self.mock_permalink.return_value}',
|
||||
'site': site,
|
||||
'site_id': site.id
|
||||
'site_id': site.id,
|
||||
'push_notification_extra_context': {
|
||||
'notification_type': 'forum_response',
|
||||
'topic_id': thread['commentable_id'],
|
||||
'course_id': comment['course_id'],
|
||||
'parent_id': str(comment['parent_id']),
|
||||
'thread_id': thread['id'],
|
||||
'comment_id': comment['id'],
|
||||
},
|
||||
})
|
||||
expected_recipient = Recipient(self.thread_author.id, self.thread_author.email)
|
||||
actual_message = self.mock_ace_send.call_args_list[0][0][0]
|
||||
@@ -326,7 +340,9 @@ class TaskTestCase(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missin
|
||||
'comment_id': comment_dict['id'],
|
||||
'thread_id': thread['id'],
|
||||
})
|
||||
assert actual_result is False
|
||||
|
||||
should_email_send = _is_first_comment(comment_dict['id'], thread['id'])
|
||||
assert should_email_send is False
|
||||
assert not self.mock_ace_send.called
|
||||
|
||||
def test_subcomment_should_not_send_email(self):
|
||||
|
||||
@@ -142,6 +142,14 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal
|
||||
"""
|
||||
previous_state = EmailEnrollmentState(course_id, student_email)
|
||||
enrollment_obj = None
|
||||
if email_params:
|
||||
email_params.update({
|
||||
'app_label': 'instructor',
|
||||
'push_notification_extra_context': {
|
||||
'notification_type': 'enroll',
|
||||
'course_id': str(course_id),
|
||||
},
|
||||
})
|
||||
if previous_state.user and previous_state.user.is_active:
|
||||
# if the student is currently unenrolled, don't enroll them in their
|
||||
# previous mode
|
||||
@@ -195,6 +203,13 @@ def unenroll_email(course_id, student_email, email_students=False, email_params=
|
||||
representing state before and after the action.
|
||||
"""
|
||||
previous_state = EmailEnrollmentState(course_id, student_email)
|
||||
if email_params:
|
||||
email_params.update({
|
||||
'app_label': 'instructor',
|
||||
'push_notification_extra_context': {
|
||||
'notification_type': 'unenroll',
|
||||
},
|
||||
})
|
||||
if previous_state.enrollment:
|
||||
CourseEnrollment.unenroll_by_email(student_email, course_id)
|
||||
if email_students:
|
||||
@@ -233,6 +248,11 @@ def send_beta_role_email(action, user, email_params):
|
||||
email_params['email_address'] = user.email
|
||||
email_params['user_id'] = user.id
|
||||
email_params['full_name'] = user.profile.name
|
||||
email_params['app_label'] = 'instructor'
|
||||
email_params['push_notification_extra_context'] = {
|
||||
'notification_type': email_params['message_type'],
|
||||
'course_id': str(getattr(email_params.get('course'), 'id', '')),
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unexpected action received '{action}' - expected 'add' or 'remove'")
|
||||
trying_to_add_inactive_user = not user.is_active and action == 'add'
|
||||
|
||||
0
lms/djangoapps/mobile_api/notifications/__init__.py
Normal file
0
lms/djangoapps/mobile_api/notifications/__init__.py
Normal file
10
lms/djangoapps/mobile_api/notifications/urls.py
Normal file
10
lms/djangoapps/mobile_api/notifications/urls.py
Normal file
@@ -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
lms/djangoapps/mobile_api/notifications/views.py
Normal file
50
lms/djangoapps/mobile_api/notifications/views.py
Normal file
@@ -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()
|
||||
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)
|
||||
@@ -10,5 +10,6 @@ from .users.views import my_user_info
|
||||
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')),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear {{ full_name }},{% endblocktrans %}
|
||||
{% blocktrans %}You have been invited to be a beta tester for {{ course_name }} at {{ site_name }}.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been invited to a beta test for {{ course_name }} at {{ site_name }}.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear student,{% endblocktrans %}
|
||||
{% blocktrans %}You have been enrolled in {{ course_name }} at {{ site_name }}. This course will now appear on your {{ site_name }} dashboard.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been invited to register for {{ course_name }}.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear Student,{% endblocktrans %}
|
||||
{% blocktrans %}You have been unenrolled from the course {{ course_name }}. Please disregard the invitation previously sent.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear {{ full_name }},{% endblocktrans %}
|
||||
{% blocktrans %}You have been unenrolled from {{ course_name }} at {{ site_name }}. This course will no longer appear on your {{ site_name }} dashboard.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear {{ full_name }},{% endblocktrans %}
|
||||
{% blocktrans %}You have been invited to join {{ course_name }} at {{ site_name }}.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been enrolled in {{ course_name }}{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}Dear {{ full_name }},{% endblocktrans %}
|
||||
{% blocktrans %}You have been removed as a beta tester for {{ course_name }} at {{ site_name }}. This course will remain on your dashboard, but you will no longer be part of the beta testing group.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -0,0 +1,4 @@
|
||||
{% load i18n %}
|
||||
{% autoescape off %}
|
||||
{% blocktrans %}You have been removed as a beta tester for {{ course_name }} at {{ site_name }}.{% endblocktrans %}
|
||||
{% endautoescape %}
|
||||
@@ -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'
|
||||
]
|
||||
@@ -22,3 +25,30 @@ 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'
|
||||
|
||||
settings.ACE_CHANNEL_DEFAULT_PUSH = 'push_notification'
|
||||
# Note: To local development with Firebase, you must set FIREBASE_CREDENTIALS_PATH
|
||||
# (path to json file with FIREBASE_CREDENTIALS)
|
||||
# or FIREBASE_CREDENTIALS dictionary.
|
||||
settings.FIREBASE_CREDENTIALS_PATH = None
|
||||
settings.FIREBASE_CREDENTIALS = None
|
||||
|
||||
settings.FIREBASE_APP = setup_firebase_app(
|
||||
settings.FIREBASE_CREDENTIALS_PATH or settings.FIREBASE_CREDENTIALS, settings.FCM_APP_NAME
|
||||
)
|
||||
|
||||
if getattr(settings, 'FIREBASE_APP', None):
|
||||
settings.ACE_ENABLED_CHANNELS.append(settings.ACE_CHANNEL_DEFAULT_PUSH)
|
||||
settings.ACE_ENABLED_POLICIES.append('bulk_push_notification_optout')
|
||||
|
||||
settings.PUSH_NOTIFICATIONS_SETTINGS = {
|
||||
'CONFIG': 'push_notifications.conf.AppConfig',
|
||||
'APPLICATIONS': {
|
||||
settings.FCM_APP_NAME: {
|
||||
'PLATFORM': 'FCM',
|
||||
'FIREBASE_APP': settings.FIREBASE_APP,
|
||||
},
|
||||
},
|
||||
'UPDATE_ON_DUPLICATE_REG_ID': True,
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
@@ -26,3 +27,26 @@ 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', settings.FCM_APP_NAME)
|
||||
settings.FIREBASE_CREDENTIALS_PATH = settings.ENV_TOKENS.get(
|
||||
'FIREBASE_CREDENTIALS_PATH', settings.FIREBASE_CREDENTIALS_PATH
|
||||
)
|
||||
settings.FIREBASE_CREDENTIALS = settings.ENV_TOKENS.get('FIREBASE_CREDENTIALS', settings.FIREBASE_CREDENTIALS)
|
||||
|
||||
settings.FIREBASE_APP = setup_firebase_app(
|
||||
settings.FIREBASE_CREDENTIALS_PATH or settings.FIREBASE_CREDENTIALS, settings.FCM_APP_NAME
|
||||
)
|
||||
if settings.FIREBASE_APP:
|
||||
settings.ACE_ENABLED_CHANNELS.append(settings.ACE_CHANNEL_DEFAULT_PUSH)
|
||||
settings.ACE_ENABLED_POLICIES.append('bulk_push_notification_optout')
|
||||
|
||||
settings.PUSH_NOTIFICATIONS_SETTINGS = {
|
||||
'CONFIG': 'push_notifications.conf.AppConfig',
|
||||
'APPLICATIONS': {
|
||||
settings.FCM_APP_NAME: {
|
||||
'PLATFORM': 'FCM',
|
||||
'FIREBASE_APP': settings.FIREBASE_APP,
|
||||
},
|
||||
},
|
||||
'UPDATE_ON_DUPLICATE_REG_ID': True,
|
||||
}
|
||||
|
||||
25
openedx/core/djangoapps/ace_common/utils.py
Normal file
25
openedx/core/djangoapps/ace_common/utils.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
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:
|
||||
try:
|
||||
app = firebase_admin.get_app(app_name)
|
||||
except ValueError:
|
||||
certificate = firebase_admin.credentials.Certificate(firebase_credentials)
|
||||
app = firebase_admin.initialize_app(certificate, name=app_name)
|
||||
return app
|
||||
@@ -1,5 +1,5 @@
|
||||
# pylint: disable=missing-docstring,protected-access
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from openedx.core.djangoapps.django_comment_common.comment_client import models, settings
|
||||
|
||||
@@ -99,6 +99,14 @@ class Comment(models.Model):
|
||||
)
|
||||
voteable._update_from_response(response)
|
||||
|
||||
@property
|
||||
def body_text(self):
|
||||
"""
|
||||
Return the text content of the comment html body.
|
||||
"""
|
||||
soup = BeautifulSoup(self.body, 'html.parser')
|
||||
return soup.get_text()
|
||||
|
||||
|
||||
def _url_for_thread_comments(thread_id):
|
||||
return f"{settings.PREFIX}/threads/{thread_id}/comments"
|
||||
|
||||
41
openedx/core/djangoapps/notifications/policies.py
Normal file
41
openedx/core/djangoapps/notifications/policies.py
Normal file
@@ -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('push_notification_extra_context', {})):
|
||||
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())
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications
|
||||
# via -r requirements/edx/github.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
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
-e git+https://github.com/anupdhabarde/edx-proctoring-proctortrack.git@31c6c9923a51c903ae83760ecbbac191363aa2a2#egg=edx_proctoring_proctortrack
|
||||
# via
|
||||
# -r requirements/edx/doc.txt
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications
|
||||
# via -r requirements/edx/base.txt
|
||||
-e git+https://github.com/anupdhabarde/edx-proctoring-proctortrack.git@31c6c9923a51c903ae83760ecbbac191363aa2a2#egg=edx_proctoring_proctortrack
|
||||
# via -r requirements/edx/base.txt
|
||||
accessible-pygments==0.0.5
|
||||
|
||||
@@ -90,3 +90,5 @@
|
||||
# django42 support PR merged but new release is pending.
|
||||
# https://github.com/openedx/edx-platform/issues/33431
|
||||
-e git+https://github.com/anupdhabarde/edx-proctoring-proctortrack.git@31c6c9923a51c903ae83760ecbbac191363aa2a2#egg=edx_proctoring_proctortrack
|
||||
|
||||
-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# make upgrade
|
||||
#
|
||||
-e git+https://github.com/jazzband/django-push-notifications.git@0f7918136b5e6a9aec83d6513aad5b0f12143a9f#egg=django_push_notifications
|
||||
# via -r requirements/edx/base.txt
|
||||
-e git+https://github.com/anupdhabarde/edx-proctoring-proctortrack.git@31c6c9923a51c903ae83760ecbbac191363aa2a2#egg=edx_proctoring_proctortrack
|
||||
# via -r requirements/edx/base.txt
|
||||
acid-xblock==0.3.1
|
||||
|
||||
3
setup.py
3
setup.py
@@ -129,7 +129,8 @@ setup(
|
||||
'discussions_link = openedx.core.djangoapps.discussions.transformers:DiscussionsTopicLinkTransformer',
|
||||
],
|
||||
"openedx.ace.policy": [
|
||||
"bulk_email_optout = lms.djangoapps.bulk_email.policies:CourseEmailOptout"
|
||||
"bulk_email_optout = lms.djangoapps.bulk_email.policies:CourseEmailOptout",
|
||||
"bulk_push_notification_optout = openedx.core.djangoapps.notifications.policies:CoursePushNotificationOptout", # lint-amnesty, pylint: disable=line-too-long
|
||||
],
|
||||
"openedx.call_to_action": [
|
||||
"personalized_learner_schedules = openedx.features.personalized_learner_schedules.call_to_action:PersonalizedLearnerScheduleCallToAction" # lint-amnesty, pylint: disable=line-too-long
|
||||
|
||||
Reference in New Issue
Block a user