Skip to content

Commit

Permalink
feat: [FC-0047] add settings for edx-ace push notifications
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NiedielnitsevIvan committed Jun 12, 2024
1 parent 221e333 commit b325343
Show file tree
Hide file tree
Showing 36 changed files with 380 additions and 20 deletions.
2 changes: 2 additions & 0 deletions lms/djangoapps/discussion/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,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,
Expand Down
71 changes: 63 additions & 8 deletions lms/djangoapps/discussion/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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
Expand Down Expand Up @@ -74,6 +75,12 @@ def __init__(self, *args, **kwargs):
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'])
Expand All @@ -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'])
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}
{% blocktrans trimmed %}{{ comment_username }} commented to {{ thread_title }}:{% endblocktrans %}
{{ comment_body_text }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}

{% blocktrans %}Comment to {{ thread_title }}{% endblocktrans %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}{% endblocktrans %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Response to {{ thread_title }}{% endblocktrans %}
22 changes: 19 additions & 3 deletions lms/djangoapps/discussion/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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
Expand Down Expand Up @@ -222,6 +222,8 @@ def setUp(self):

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)
Expand All @@ -231,10 +233,12 @@ def tearDown(self):
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
Expand Down Expand Up @@ -271,8 +275,10 @@ def test_send_discussion_email_notification(self, user_subscribed):
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,
Expand All @@ -283,7 +289,15 @@ def test_send_discussion_email_notification(self, user_subscribed):
'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]
Expand Down Expand Up @@ -326,7 +340,9 @@ def run_should_not_send_email_test(self, thread, comment_dict):
'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):
Expand Down
20 changes: 20 additions & 0 deletions lms/djangoapps/instructor/enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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
Expand Down Expand Up @@ -194,6 +202,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:
Expand Down Expand Up @@ -232,6 +247,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'
Expand Down
Empty file.
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()
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')),
]
5 changes: 5 additions & 0 deletions lms/templates/instructor/edx_ace/addbetatester/push/body.txt
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -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 %}
5 changes: 5 additions & 0 deletions lms/templates/instructor/edx_ace/allowedenroll/push/body.txt
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}You have been invited to register for {{ course_name }}.{% endblocktrans %}
{% endautoescape %}
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %}
{% endautoescape %}
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %}
{% endautoescape %}
5 changes: 5 additions & 0 deletions lms/templates/instructor/edx_ace/enrollenrolled/push/body.txt
Original file line number Diff line number Diff line change
@@ -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 %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}You have been enrolled in {{ course_name }}{% endblocktrans %}
{% endautoescape %}
Original file line number Diff line number Diff line change
@@ -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 %}
Loading

0 comments on commit b325343

Please sign in to comment.