From 28eb406f8daea86226f630d98f9fc5882fef447f Mon Sep 17 00:00:00 2001 From: Ivan Niedielnitsev <81557788+niedielnitsevivan@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:42:04 +0200 Subject: [PATCH 01/17] 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 --- lms/djangoapps/discussion/signals/handlers.py | 2 + lms/djangoapps/discussion/tasks.py | 71 ++++++++++++++++--- .../edx_ace/commentnotification/push/body.txt | 3 + .../commentnotification/push/subject.txt | 3 + .../responsenotification/push/body.txt | 2 + .../responsenotification/push/subject.txt | 2 + lms/djangoapps/discussion/tests/test_tasks.py | 22 +++++- lms/djangoapps/instructor/enrollment.py | 20 ++++++ .../mobile_api/notifications/__init__.py | 0 .../mobile_api/notifications/urls.py | 10 +++ .../mobile_api/notifications/views.py | 50 +++++++++++++ lms/djangoapps/mobile_api/urls.py | 1 + .../edx_ace/addbetatester/push/body.txt | 5 ++ .../edx_ace/addbetatester/push/subject.txt | 4 ++ .../edx_ace/allowedenroll/push/body.txt | 5 ++ .../edx_ace/allowedenroll/push/subject.txt | 4 ++ .../edx_ace/allowedunenroll/push/body.txt | 5 ++ .../edx_ace/allowedunenroll/push/subject.txt | 4 ++ .../edx_ace/enrolledunenroll/push/body.txt | 5 ++ .../edx_ace/enrolledunenroll/push/subject.txt | 4 ++ .../edx_ace/enrollenrolled/push/body.txt | 5 ++ .../edx_ace/enrollenrolled/push/subject.txt | 4 ++ .../edx_ace/removebetatester/push/body.txt | 5 ++ .../edx_ace/removebetatester/push/subject.txt | 4 ++ .../djangoapps/ace_common/settings/common.py | 30 ++++++++ .../ace_common/settings/production.py | 24 +++++++ openedx/core/djangoapps/ace_common/utils.py | 25 +++++++ .../comment_client/comment.py | 10 ++- .../core/djangoapps/notifications/policies.py | 41 +++++++++++ requirements/edx/base.txt | 2 + requirements/edx/development.txt | 4 ++ requirements/edx/doc.txt | 2 + requirements/edx/github.in | 2 + requirements/edx/testing.txt | 2 + setup.py | 3 +- 35 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/body.txt create mode 100644 lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt create mode 100644 lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt create mode 100644 lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt create mode 100644 lms/djangoapps/mobile_api/notifications/__init__.py create mode 100644 lms/djangoapps/mobile_api/notifications/urls.py create mode 100644 lms/djangoapps/mobile_api/notifications/views.py create mode 100644 lms/templates/instructor/edx_ace/addbetatester/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/addbetatester/push/subject.txt create mode 100644 lms/templates/instructor/edx_ace/allowedenroll/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt create mode 100644 lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt create mode 100644 lms/templates/instructor/edx_ace/enrolledunenroll/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt create mode 100644 lms/templates/instructor/edx_ace/enrollenrolled/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt create mode 100644 lms/templates/instructor/edx_ace/removebetatester/push/body.txt create mode 100644 lms/templates/instructor/edx_ace/removebetatester/push/subject.txt create mode 100644 openedx/core/djangoapps/ace_common/utils.py create mode 100644 openedx/core/djangoapps/notifications/policies.py diff --git a/lms/djangoapps/discussion/signals/handlers.py b/lms/djangoapps/discussion/signals/handlers.py index 2aa7d36456c4..73c19d27858c 100644 --- a/lms/djangoapps/discussion/signals/handlers.py +++ b/lms/djangoapps/discussion/signals/handlers.py @@ -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, diff --git a/lms/djangoapps/discussion/tasks.py b/lms/djangoapps/discussion/tasks.py index d483a82dbd66..23a641fae958 100644 --- a/lms/djangoapps/discussion/tasks.py +++ b/lms/djangoapps/discussion/tasks.py @@ -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 @@ -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 @@ -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']) }) diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/body.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/body.txt new file mode 100644 index 000000000000..391e3d8ef4d7 --- /dev/null +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/body.txt @@ -0,0 +1,3 @@ +{% load i18n %} +{% blocktrans trimmed %}{{ comment_username }} commented to {{ thread_title }}:{% endblocktrans %} +{{ comment_body_text }} diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt new file mode 100644 index 000000000000..d2298a812990 --- /dev/null +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt @@ -0,0 +1,3 @@ +{% load i18n %} + +{% blocktrans %}Comment to {{ thread_title }}{% endblocktrans %} diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt new file mode 100644 index 000000000000..c1fe3ba35b7f --- /dev/null +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}{% endblocktrans %} diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt new file mode 100644 index 000000000000..03caca997346 --- /dev/null +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans %}Response to {{ thread_title }}{% endblocktrans %} diff --git a/lms/djangoapps/discussion/tests/test_tasks.py b/lms/djangoapps/discussion/tests/test_tasks.py index f6cce4437546..950950210d47 100644 --- a/lms/djangoapps/discussion/tests/test_tasks.py +++ b/lms/djangoapps/discussion/tests/test_tasks.py @@ -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 @@ -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) @@ -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 @@ -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, @@ -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] @@ -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): diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 896d0deadcd9..2c4294cad98b 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -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' diff --git a/lms/djangoapps/mobile_api/notifications/__init__.py b/lms/djangoapps/mobile_api/notifications/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/mobile_api/notifications/urls.py b/lms/djangoapps/mobile_api/notifications/urls.py new file mode 100644 index 000000000000..17b970916a47 --- /dev/null +++ b/lms/djangoapps/mobile_api/notifications/urls.py @@ -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'), +] diff --git a/lms/djangoapps/mobile_api/notifications/views.py b/lms/djangoapps/mobile_api/notifications/views.py new file mode 100644 index 000000000000..4c94ae576e76 --- /dev/null +++ b/lms/djangoapps/mobile_api/notifications/views.py @@ -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) diff --git a/lms/djangoapps/mobile_api/urls.py b/lms/djangoapps/mobile_api/urls.py index 1ad34ced5de9..c7aacc0b669a 100644 --- a/lms/djangoapps/mobile_api/urls.py +++ b/lms/djangoapps/mobile_api/urls.py @@ -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')), ] diff --git a/lms/templates/instructor/edx_ace/addbetatester/push/body.txt b/lms/templates/instructor/edx_ace/addbetatester/push/body.txt new file mode 100644 index 000000000000..8373638fb41f --- /dev/null +++ b/lms/templates/instructor/edx_ace/addbetatester/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/addbetatester/push/subject.txt b/lms/templates/instructor/edx_ace/addbetatester/push/subject.txt new file mode 100644 index 000000000000..f1c4c6826cfa --- /dev/null +++ b/lms/templates/instructor/edx_ace/addbetatester/push/subject.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt b/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt new file mode 100644 index 000000000000..14e4915f86e2 --- /dev/null +++ b/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt b/lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt new file mode 100644 index 000000000000..865657f1fcb1 --- /dev/null +++ b/lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}You have been invited to register for {{ course_name }}.{% endblocktrans %} +{% endautoescape %} diff --git a/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt b/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt new file mode 100644 index 000000000000..b825ce1d4d18 --- /dev/null +++ b/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt b/lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt new file mode 100644 index 000000000000..99aaa1a9c305 --- /dev/null +++ b/lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %} +{% endautoescape %} diff --git a/lms/templates/instructor/edx_ace/enrolledunenroll/push/body.txt b/lms/templates/instructor/edx_ace/enrolledunenroll/push/body.txt new file mode 100644 index 000000000000..2bc61a840b48 --- /dev/null +++ b/lms/templates/instructor/edx_ace/enrolledunenroll/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt b/lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt new file mode 100644 index 000000000000..99aaa1a9c305 --- /dev/null +++ b/lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}You have been unenrolled from {{ course_name }}{% endblocktrans %} +{% endautoescape %} diff --git a/lms/templates/instructor/edx_ace/enrollenrolled/push/body.txt b/lms/templates/instructor/edx_ace/enrollenrolled/push/body.txt new file mode 100644 index 000000000000..e5ef12dc5f75 --- /dev/null +++ b/lms/templates/instructor/edx_ace/enrollenrolled/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt b/lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt new file mode 100644 index 000000000000..ebe884b30f08 --- /dev/null +++ b/lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% autoescape off %} +{% blocktrans %}You have been enrolled in {{ course_name }}{% endblocktrans %} +{% endautoescape %} diff --git a/lms/templates/instructor/edx_ace/removebetatester/push/body.txt b/lms/templates/instructor/edx_ace/removebetatester/push/body.txt new file mode 100644 index 000000000000..89573aa4be1d --- /dev/null +++ b/lms/templates/instructor/edx_ace/removebetatester/push/body.txt @@ -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 %} diff --git a/lms/templates/instructor/edx_ace/removebetatester/push/subject.txt b/lms/templates/instructor/edx_ace/removebetatester/push/subject.txt new file mode 100644 index 000000000000..c09febbb455c --- /dev/null +++ b/lms/templates/instructor/edx_ace/removebetatester/push/subject.txt @@ -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 %} diff --git a/openedx/core/djangoapps/ace_common/settings/common.py b/openedx/core/djangoapps/ace_common/settings/common.py index 11bfbce5c59f..993412f392bb 100644 --- a/openedx/core/djangoapps/ace_common/settings/common.py +++ b/openedx/core/djangoapps/ace_common/settings/common.py @@ -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, + } diff --git a/openedx/core/djangoapps/ace_common/settings/production.py b/openedx/core/djangoapps/ace_common/settings/production.py index cc4da91c18db..48da799f00b4 100644 --- a/openedx/core/djangoapps/ace_common/settings/production.py +++ b/openedx/core/djangoapps/ace_common/settings/production.py @@ -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, + } diff --git a/openedx/core/djangoapps/ace_common/utils.py b/openedx/core/djangoapps/ace_common/utils.py new file mode 100644 index 000000000000..508ac4033cd1 --- /dev/null +++ b/openedx/core/djangoapps/ace_common/utils.py @@ -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 diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py index 0b7a695a1c3e..c86f7eb40515 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/comment.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/comment.py @@ -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 @@ def unFlagAbuse(self, user, voteable, removeAll): ) 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" diff --git a/openedx/core/djangoapps/notifications/policies.py b/openedx/core/djangoapps/notifications/policies.py new file mode 100644 index 000000000000..8467eda9324e --- /dev/null +++ b/openedx/core/djangoapps/notifications/policies.py @@ -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()) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 85167e0d36ca..755e51d26fc8 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.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/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 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 876bc4fcc4a2..9c6bbbc2bbf2 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -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 diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index a222e78518ed..971dc13b06cf 100644 --- a/requirements/edx/doc.txt +++ b/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 diff --git a/requirements/edx/github.in b/requirements/edx/github.in index 6ec36d3a0681..f4549c90c1ee 100644 --- a/requirements/edx/github.in +++ b/requirements/edx/github.in @@ -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 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index ac468974eb6c..0d694458d1b8 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.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 acid-xblock==0.3.1 diff --git a/setup.py b/setup.py index bf662b563c7f..55863c5d07fb 100644 --- a/setup.py +++ b/setup.py @@ -129,7 +129,8 @@ '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 From ce21022fe26ed9485e6be03cb0e66d8b2bf1d12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Wed, 3 Jul 2024 10:12:32 +0300 Subject: [PATCH 02/17] refactor: [FC-0047] rename subject files to title --- .../edx_ace/commentnotification/push/{subject.txt => title.txt} | 1 - .../edx_ace/responsenotification/push/{subject.txt => title.txt} | 0 .../edx_ace/addbetatester/push/{subject.txt => title.txt} | 0 .../edx_ace/allowedenroll/push/{subject.txt => title.txt} | 0 .../edx_ace/allowedunenroll/push/{subject.txt => title.txt} | 0 .../edx_ace/enrolledunenroll/push/{subject.txt => title.txt} | 0 .../edx_ace/enrollenrolled/push/{subject.txt => title.txt} | 0 .../edx_ace/removebetatester/push/{subject.txt => title.txt} | 0 8 files changed, 1 deletion(-) rename lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/{subject.txt => title.txt} (98%) rename lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/addbetatester/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/allowedenroll/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/allowedunenroll/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/enrolledunenroll/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/enrollenrolled/push/{subject.txt => title.txt} (100%) rename lms/templates/instructor/edx_ace/removebetatester/push/{subject.txt => title.txt} (100%) diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/title.txt similarity index 98% rename from lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt rename to lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/title.txt index d2298a812990..a9ea6f298c03 100644 --- a/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/subject.txt +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/commentnotification/push/title.txt @@ -1,3 +1,2 @@ {% load i18n %} - {% blocktrans %}Comment to {{ thread_title }}{% endblocktrans %} diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/title.txt similarity index 100% rename from lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/subject.txt rename to lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/title.txt diff --git a/lms/templates/instructor/edx_ace/addbetatester/push/subject.txt b/lms/templates/instructor/edx_ace/addbetatester/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/addbetatester/push/subject.txt rename to lms/templates/instructor/edx_ace/addbetatester/push/title.txt diff --git a/lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt b/lms/templates/instructor/edx_ace/allowedenroll/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/allowedenroll/push/subject.txt rename to lms/templates/instructor/edx_ace/allowedenroll/push/title.txt diff --git a/lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt b/lms/templates/instructor/edx_ace/allowedunenroll/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/allowedunenroll/push/subject.txt rename to lms/templates/instructor/edx_ace/allowedunenroll/push/title.txt diff --git a/lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt b/lms/templates/instructor/edx_ace/enrolledunenroll/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/enrolledunenroll/push/subject.txt rename to lms/templates/instructor/edx_ace/enrolledunenroll/push/title.txt diff --git a/lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt b/lms/templates/instructor/edx_ace/enrollenrolled/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/enrollenrolled/push/subject.txt rename to lms/templates/instructor/edx_ace/enrollenrolled/push/title.txt diff --git a/lms/templates/instructor/edx_ace/removebetatester/push/subject.txt b/lms/templates/instructor/edx_ace/removebetatester/push/title.txt similarity index 100% rename from lms/templates/instructor/edx_ace/removebetatester/push/subject.txt rename to lms/templates/instructor/edx_ace/removebetatester/push/title.txt From 742a8786980976296ed3118ff5bb8157474d8616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Wed, 3 Jul 2024 10:23:41 +0300 Subject: [PATCH 03/17] docs: [FC-0047] add docs for setting up mobile push notifications --- .../docs/push_notifications_configuration.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md diff --git a/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md b/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md new file mode 100644 index 000000000000..4455ce9f3c55 --- /dev/null +++ b/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md @@ -0,0 +1,63 @@ +# Configure mobile push notifications in edx-platform + + +### 1. Create a new Firebase project + +All push notifications in Open edX are sent via FCM service to start with it you need to create +a new Firebase project in Firebase console https://console.firebase.google.com/ + +### 2. Provide service account credentials to initialize an FCM admin application in edx-platform + +To configure sending push notifications via FCM from edx-platform, you need to generate private +key for Firebase admin SDK in Project settings > Service accounts section. + +After downloading .json key, you should mount it to LMS/CMS containers and specify a path to +the mounted file using FIREBASE_CREDENTIALS_PATH settings +[variable](https://github.com/openedx/edx-platform/pull/34971/files#diff-f694c479e5c9b133241a799e1ddf33d5d5133bfdec91e3f7d371e094c9999e74R31). There is also an alternative option, +which is to add the value from the .json key to the FIREBASE_CREDENTIALS environment +[variable](https://github.com/openedx/edx-platform/pull/34971/files#diff-f694c479e5c9b133241a799e1ddf33d5d5133bfdec91e3f7d371e094c9999e74R34), +like a python dictionary. + +https://github.com/openedx/edx-ace/blob/master/docs/decisions/0002-push-notifications.rst?plain=1#L108 + + +### 3. Configure and build mobile applications + +Use the supported Open edX mobile applications: + +https://github.com/openedx/openedx-app-android/ + +https://github.com/openedx/openedx-app-ios + +#### 3.1 Configure oauth2 + +First you need to configure Oauth applications for each mobile client in edx-platform. You should create separate +entries for Android and IOS applications in the Django OAuth Toolkit > Applications. + +Fill in all required fields in the form: + - Client ID: . + - Client type: Public + - Authorization grant type: Resource owner password-based + - Public Client secret: + +Specify generated Client ID in mobile config.yaml file + +https://github.com/openedx/openedx-app-android/blob/main/Documentation/ConfigurationManagement.md#configuration-files + +https://github.com/openedx/openedx-app-ios/blob/main/Documentation/CONFIGURATION_MANAGEMENT.md#examples-of-config-files + +#### 3.2 Provide FCM credentials to the app + +Create new apps in Firebase Console for Android and IOS in Project settings > General section. + +Download credentials file, google-services.json for Android, or GoogleService-Info.plist for IOS. + +Copy/paste values from configuration file into config.yaml as shown in example configurations. + +https://github.com/openedx/openedx-app-android/blob/main/Documentation/ConfigurationManagement.md#configuration-files + +https://github.com/openedx/openedx-app-ios/blob/main/Documentation/CONFIGURATION_MANAGEMENT.md#examples-of-config-files + +Build applications and you’re ready to go! + From 84d0b2a1a315e118fc0030d7d62c81fd5de5437b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Tue, 30 Jul 2024 20:23:35 +0300 Subject: [PATCH 04/17] chore: [FC-0047] upgrade requirements --- requirements/edx/github.in | 2 -- requirements/edx/kernel.in | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements/edx/github.in b/requirements/edx/github.in index f4549c90c1ee..6ec36d3a0681 100644 --- a/requirements/edx/github.in +++ b/requirements/edx/github.in @@ -90,5 +90,3 @@ # 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 diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index a5b510742ac7..fcb740c8f323 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -47,6 +47,7 @@ django-mptt django-mysql django-oauth-toolkit # Provides oAuth2 capabilities for Django django-pipeline +django-push-notifications django-ratelimit django-sekizai django-simple-history From 9a0734b28004f11555b33c746db4b2ea3422105e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Thu, 1 Aug 2024 17:36:46 +0300 Subject: [PATCH 05/17] style: [FC-0047] add module docstrings --- lms/djangoapps/mobile_api/notifications/urls.py | 3 +++ lms/djangoapps/mobile_api/notifications/views.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lms/djangoapps/mobile_api/notifications/urls.py b/lms/djangoapps/mobile_api/notifications/urls.py index 17b970916a47..b0fe46a86046 100644 --- a/lms/djangoapps/mobile_api/notifications/urls.py +++ b/lms/djangoapps/mobile_api/notifications/urls.py @@ -1,3 +1,6 @@ +""" +URLs for the mobile_api.notifications APIs. +""" from django.urls import path from .views import GCMDeviceViewSet diff --git a/lms/djangoapps/mobile_api/notifications/views.py b/lms/djangoapps/mobile_api/notifications/views.py index 4c94ae576e76..4778dc83c99c 100644 --- a/lms/djangoapps/mobile_api/notifications/views.py +++ b/lms/djangoapps/mobile_api/notifications/views.py @@ -1,3 +1,6 @@ +""" +This module contains the view for registering a device for push notifications. +""" from django.conf import settings from rest_framework import status from rest_framework.response import Response From 9d0c45680e25fe91796d3a8a818d4fa6638f10b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Fri, 9 Aug 2024 12:50:46 +0300 Subject: [PATCH 06/17] refactor: [FC-0047] fix review issues --- lms/djangoapps/discussion/tasks.py | 4 +-- lms/djangoapps/discussion/tests/test_tasks.py | 8 ++++-- lms/djangoapps/instructor/enrollment.py | 26 +++++++++---------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/lms/djangoapps/discussion/tasks.py b/lms/djangoapps/discussion/tasks.py index 23a641fae958..65a03c687a49 100644 --- a/lms/djangoapps/discussion/tasks.py +++ b/lms/djangoapps/discussion/tasks.py @@ -76,9 +76,7 @@ def __init__(self, *args, **kwargs): class CommentNotification(BaseMessageType): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.options['transactional'] = True + pass @shared_task(base=LoggedTask) diff --git a/lms/djangoapps/discussion/tests/test_tasks.py b/lms/djangoapps/discussion/tests/test_tasks.py index 950950210d47..92dadac9d9ee 100644 --- a/lms/djangoapps/discussion/tests/test_tasks.py +++ b/lms/djangoapps/discussion/tests/test_tasks.py @@ -19,7 +19,11 @@ 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 _is_first_comment, _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 @@ -342,7 +346,7 @@ def run_should_not_send_email_test(self, thread, comment_dict): }) should_email_send = _is_first_comment(comment_dict['id'], thread['id']) - assert should_email_send is False + assert not should_email_send assert not self.mock_ace_send.called def test_subcomment_should_not_send_email(self): diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 2c4294cad98b..4abf68dec060 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -125,7 +125,7 @@ def get_user_email_language(user): return UserPreference.get_value(user, LANGUAGE_KEY) -def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None, language=None): +def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, message_params=None, language=None): """ Enroll a student by email. @@ -134,7 +134,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal if auto_enroll is set, then when the email registers, they will be enrolled in the course automatically. `email_students` determines if student should be notified of action by email. - `email_params` parameters used while parsing email templates (a `dict`). + `message_params` parameters used while parsing message templates (a `dict`). `language` is the language used to render the email. returns two EmailEnrollmentState's @@ -142,8 +142,8 @@ 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({ + if message_params: + message_params.update({ 'app_label': 'instructor', 'push_notification_extra_context': { 'notification_type': 'enroll', @@ -168,22 +168,22 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal enrollment_obj = CourseEnrollment.enroll_by_email(student_email, course_id, course_mode) if email_students: - email_params['message_type'] = 'enrolled_enroll' - email_params['email_address'] = student_email - email_params['user_id'] = previous_state.user.id - email_params['full_name'] = previous_state.full_name - send_mail_to_student(student_email, email_params, language=language) + message_params['message_type'] = 'enrolled_enroll' + message_params['email_address'] = student_email + message_params['user_id'] = previous_state.user.id + message_params['full_name'] = previous_state.full_name + send_mail_to_student(student_email, message_params, language=language) elif not is_email_retired(student_email): cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=student_email) cea.auto_enroll = auto_enroll cea.save() if email_students: - email_params['message_type'] = 'allowed_enroll' - email_params['email_address'] = student_email + message_params['message_type'] = 'allowed_enroll' + message_params['email_address'] = student_email if previous_state.user: - email_params['user_id'] = previous_state.user.id - send_mail_to_student(student_email, email_params, language=language) + message_params['user_id'] = previous_state.user.id + send_mail_to_student(student_email, message_params, language=language) after_state = EmailEnrollmentState(course_id, student_email) From 69078f3eed5903c9be465f0bf72d634fb7dbaa4f Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Sat, 10 Aug 2024 16:11:53 +0300 Subject: [PATCH 07/17] refactor: change name of the policy to course push optout --- openedx/core/djangoapps/ace_common/settings/common.py | 2 +- openedx/core/djangoapps/ace_common/settings/production.py | 2 +- openedx/core/djangoapps/notifications/policies.py | 2 +- setup.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/ace_common/settings/common.py b/openedx/core/djangoapps/ace_common/settings/common.py index 993412f392bb..634ab328ba6b 100644 --- a/openedx/core/djangoapps/ace_common/settings/common.py +++ b/openedx/core/djangoapps/ace_common/settings/common.py @@ -40,7 +40,7 @@ def plugin_settings(settings): # lint-amnesty, pylint: disable=missing-function 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.ACE_ENABLED_POLICIES.append('course_push_notification_optout') settings.PUSH_NOTIFICATIONS_SETTINGS = { 'CONFIG': 'push_notifications.conf.AppConfig', diff --git a/openedx/core/djangoapps/ace_common/settings/production.py b/openedx/core/djangoapps/ace_common/settings/production.py index 48da799f00b4..9ff56292012e 100644 --- a/openedx/core/djangoapps/ace_common/settings/production.py +++ b/openedx/core/djangoapps/ace_common/settings/production.py @@ -38,7 +38,7 @@ def plugin_settings(settings): ) if settings.FIREBASE_APP: settings.ACE_ENABLED_CHANNELS.append(settings.ACE_CHANNEL_DEFAULT_PUSH) - settings.ACE_ENABLED_POLICIES.append('bulk_push_notification_optout') + settings.ACE_ENABLED_POLICIES.append('course_push_notification_optout') settings.PUSH_NOTIFICATIONS_SETTINGS = { 'CONFIG': 'push_notifications.conf.AppConfig', diff --git a/openedx/core/djangoapps/notifications/policies.py b/openedx/core/djangoapps/notifications/policies.py index 8467eda9324e..8d0a2d8d43a5 100644 --- a/openedx/core/djangoapps/notifications/policies.py +++ b/openedx/core/djangoapps/notifications/policies.py @@ -16,7 +16,7 @@ def check(self, message): """ Check if the user has opted out of push notifications for the given course. :param message: - :return: + :return: PolicyResult """ course_ids = message.context.get('course_ids', []) app_label = message.context.get('app_label') diff --git a/setup.py b/setup.py index 55863c5d07fb..28a25cc91476 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,7 @@ ], "openedx.ace.policy": [ "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 + "course_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 From c7235e34e25f118760fd4209f76027d9ce528ae0 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Sat, 10 Aug 2024 16:22:51 +0300 Subject: [PATCH 08/17] build: remove diff for requirements --- requirements/edx/kernel.in | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index fcb740c8f323..a5b510742ac7 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -47,7 +47,6 @@ django-mptt django-mysql django-oauth-toolkit # Provides oAuth2 capabilities for Django django-pipeline -django-push-notifications django-ratelimit django-sekizai django-simple-history From 8a7054429af51f06ec9bcceda32ab503d7f49d51 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Sat, 10 Aug 2024 18:42:49 +0300 Subject: [PATCH 09/17] fix: rename email to message params --- lms/djangoapps/ccx/api/v0/tests/test_views.py | 4 +- lms/djangoapps/ccx/api/v0/views.py | 8 +-- lms/djangoapps/ccx/utils.py | 20 +++--- lms/djangoapps/ccx/views.py | 4 +- lms/djangoapps/instructor/access.py | 4 +- lms/djangoapps/instructor/enrollment.py | 66 ++++++++++--------- 6 files changed, 55 insertions(+), 51 deletions(-) diff --git a/lms/djangoapps/ccx/api/v0/tests/test_views.py b/lms/djangoapps/ccx/api/v0/tests/test_views.py index 7279b9426347..a8c9070df038 100644 --- a/lms/djangoapps/ccx/api/v0/tests/test_views.py +++ b/lms/djangoapps/ccx/api/v0/tests/test_views.py @@ -730,8 +730,8 @@ def make_ccx(self, max_students_allowed=200): course_id=ccx_course_key, student_email=self.coach.email, auto_enroll=True, - email_students=False, - email_params=email_params, + message_students=False, + message_params=email_params, ) return ccx diff --git a/lms/djangoapps/ccx/api/v0/views.py b/lms/djangoapps/ccx/api/v0/views.py index b3e345a77022..8ca15e065006 100644 --- a/lms/djangoapps/ccx/api/v0/views.py +++ b/lms/djangoapps/ccx/api/v0/views.py @@ -505,8 +505,8 @@ def post(self, request): course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, - email_students=True, - email_params=email_params, + message_students=True, + message_params=email_params, ) # assign staff role for the coach to the newly created ccx assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) @@ -768,8 +768,8 @@ def patch(self, request, ccx_course_id=None): course_id=ccx_course_key, student_email=coach.email, auto_enroll=True, - email_students=True, - email_params=email_params, + message_students=True, + message_params=email_params, ) # make the new coach staff on the CCX assign_staff_role_to_ccx(ccx_course_key, coach, master_course_object.id) diff --git a/lms/djangoapps/ccx/utils.py b/lms/djangoapps/ccx/utils.py index 9f7c0eff3963..2afed619ddd2 100644 --- a/lms/djangoapps/ccx/utils.py +++ b/lms/djangoapps/ccx/utils.py @@ -269,7 +269,7 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke log.info("%s", error) errors.append(error) break - enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) + enroll_email(course_key, email, auto_enroll=True, message_students=email_students, message_params=email_params) elif action == 'Unenroll' or action == 'revoke': # lint-amnesty, pylint: disable=consider-using-in for identifier in identifiers: try: @@ -278,7 +278,7 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke log.info("%s", exp) errors.append(f"{exp}") continue - unenroll_email(course_key, email, email_students=email_students, email_params=email_params) + unenroll_email(course_key, email, message_students=email_students, message_params=email_params) return errors @@ -348,8 +348,8 @@ def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_em course_id=ccx_key, student_email=staff.email, auto_enroll=True, - email_students=send_email, - email_params=email_params, + message_students=send_email, + message_params=email_params, ) # allow 'staff' access on ccx to staff of master course @@ -373,8 +373,8 @@ def add_master_course_staff_to_ccx(master_course, ccx_key, display_name, send_em course_id=ccx_key, student_email=instructor.email, auto_enroll=True, - email_students=send_email, - email_params=email_params, + message_students=send_email, + message_params=email_params, ) # allow 'instructor' access on ccx to instructor of master course @@ -417,8 +417,8 @@ def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, se unenroll_email( course_id=ccx_key, student_email=staff.email, - email_students=send_email, - email_params=email_params, + message_students=send_email, + message_params=email_params, ) for instructor in list_instructor: @@ -430,6 +430,6 @@ def remove_master_course_staff_from_ccx(master_course, ccx_key, display_name, se unenroll_email( course_id=ccx_key, student_email=instructor.email, - email_students=send_email, - email_params=email_params, + message_students=send_email, + message_params=email_params, ) diff --git a/lms/djangoapps/ccx/views.py b/lms/djangoapps/ccx/views.py index 3c5f3130a195..7c6a75aaf6d4 100644 --- a/lms/djangoapps/ccx/views.py +++ b/lms/djangoapps/ccx/views.py @@ -223,8 +223,8 @@ def create_ccx(request, course, ccx=None): course_id=ccx_id, student_email=request.user.email, auto_enroll=True, - email_students=True, - email_params=email_params, + message_students=True, + message_params=email_params, ) assign_staff_role_to_ccx(ccx_id, request.user, course.id) diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py index 9255d113f038..a5d25769ca10 100644 --- a/lms/djangoapps/instructor/access.py +++ b/lms/djangoapps/instructor/access.py @@ -86,8 +86,8 @@ def _change_access(course, user, level, action, send_email=True): course_id=course.id, student_email=user.email, auto_enroll=True, - email_students=send_email, - email_params=email_params, + message_students=send_email, + message_params=email_params, ) role.add_users(user) elif action == 'revoke': diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 4abf68dec060..55b4b8cf1957 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -125,7 +125,7 @@ def get_user_email_language(user): return UserPreference.get_value(user, LANGUAGE_KEY) -def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, message_params=None, language=None): +def enroll_email(course_id, student_email, auto_enroll=False, message_students=False, message_params=None, language=None): """ Enroll a student by email. @@ -133,7 +133,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal `auto_enroll` determines what is put in CourseEnrollmentAllowed.auto_enroll if auto_enroll is set, then when the email registers, they will be enrolled in the course automatically. - `email_students` determines if student should be notified of action by email. + `message_students` determines if student should be notified of action by email or push message. `message_params` parameters used while parsing message templates (a `dict`). `language` is the language used to render the email. @@ -150,6 +150,8 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal 'course_id': str(course_id), }, }) + else: + message_params = {} if previous_state.user and previous_state.user.is_active: # if the student is currently unenrolled, don't enroll them in their # previous mode @@ -167,7 +169,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal course_mode = previous_state.mode enrollment_obj = CourseEnrollment.enroll_by_email(student_email, course_id, course_mode) - if email_students: + if message_students: message_params['message_type'] = 'enrolled_enroll' message_params['email_address'] = student_email message_params['user_id'] = previous_state.user.id @@ -178,7 +180,7 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=student_email) cea.auto_enroll = auto_enroll cea.save() - if email_students: + if message_students: message_params['message_type'] = 'allowed_enroll' message_params['email_address'] = student_email if previous_state.user: @@ -190,74 +192,76 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal return previous_state, after_state, enrollment_obj -def unenroll_email(course_id, student_email, email_students=False, email_params=None, language=None): +def unenroll_email(course_id, student_email, message_students=False, message_params=None, language=None): """ Unenroll a student by email. `student_email` is student's emails e.g. "foo@bar.com" - `email_students` determines if student should be notified of action by email. - `email_params` parameters used while parsing email templates (a `dict`). + `message_students` determines if student should be notified of action by email or push message. + `message_params` parameters used while parsing email templates (a `dict`). `language` is the language used to render the email. returns two EmailEnrollmentState's representing state before and after the action. """ previous_state = EmailEnrollmentState(course_id, student_email) - if email_params: - email_params.update({ + if message_params: + message_params.update({ 'app_label': 'instructor', 'push_notification_extra_context': { 'notification_type': 'unenroll', }, }) + else: + message_params = {} if previous_state.enrollment: CourseEnrollment.unenroll_by_email(student_email, course_id) - if email_students: - email_params['message_type'] = 'enrolled_unenroll' - email_params['email_address'] = student_email + if message_students: + message_params['message_type'] = 'enrolled_unenroll' + message_params['email_address'] = student_email if previous_state.user: - email_params['user_id'] = previous_state.user.id - email_params['full_name'] = previous_state.full_name - send_mail_to_student(student_email, email_params, language=language) + message_params['user_id'] = previous_state.user.id + message_params['full_name'] = previous_state.full_name + send_mail_to_student(student_email, message_params, language=language) if previous_state.allowed: CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email).delete() - if email_students: - email_params['message_type'] = 'allowed_unenroll' - email_params['email_address'] = student_email + if message_students: + message_params['message_type'] = 'allowed_unenroll' + message_params['email_address'] = student_email if previous_state.user: - email_params['user_id'] = previous_state.user.id + message_params['user_id'] = previous_state.user.id # Since no User object exists for this student there is no "full_name" available. - send_mail_to_student(student_email, email_params, language=language) + send_mail_to_student(student_email, message_params, language=language) after_state = EmailEnrollmentState(course_id, student_email) return previous_state, after_state -def send_beta_role_email(action, user, email_params): +def send_beta_role_email(action, user, message_params): """ Send an email to a user added or removed as a beta tester. `action` is one of 'add' or 'remove' `user` is the User affected - `email_params` parameters used while parsing email templates (a `dict`). + `message_params` parameters used while parsing email templates (a `dict`). """ if action in ('add', 'remove'): - email_params['message_type'] = '%s_beta_tester' % action - 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', '')), + message_params['message_type'] = '%s_beta_tester' % action + message_params['email_address'] = user.email + message_params['user_id'] = user.id + message_params['full_name'] = user.profile.name + message_params['app_label'] = 'instructor' + message_params['push_notification_extra_context'] = { + 'notification_type': message_params['message_type'], + 'course_id': str(getattr(message_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' if not trying_to_add_inactive_user: - send_mail_to_student(user.email, email_params, language=get_user_email_language(user)) + send_mail_to_student(user.email, message_params, language=get_user_email_language(user)) @contextmanager From c70a6e74cbc0757015c6634a59274bd09fdc288a Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Sat, 10 Aug 2024 19:37:48 +0300 Subject: [PATCH 10/17] fix: fix linter warning --- lms/djangoapps/instructor/enrollment.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index 55b4b8cf1957..ed344876eb42 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -125,7 +125,14 @@ def get_user_email_language(user): return UserPreference.get_value(user, LANGUAGE_KEY) -def enroll_email(course_id, student_email, auto_enroll=False, message_students=False, message_params=None, language=None): +def enroll_email( + course_id, + student_email, + auto_enroll=False, + message_students=False, + message_params=None, + language=None +): """ Enroll a student by email. From c733dc85849477706fe83a55aabb808bc0cee21b Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Sun, 11 Aug 2024 16:59:48 +0300 Subject: [PATCH 11/17] fix: remove docs from app level --- lms/djangoapps/ccx/utils.py | 8 ++- .../docs/push_notifications_configuration.md | 63 ------------------- 2 files changed, 7 insertions(+), 64 deletions(-) delete mode 100644 openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md diff --git a/lms/djangoapps/ccx/utils.py b/lms/djangoapps/ccx/utils.py index 2afed619ddd2..28ecbba34947 100644 --- a/lms/djangoapps/ccx/utils.py +++ b/lms/djangoapps/ccx/utils.py @@ -269,7 +269,13 @@ def ccx_students_enrolling_center(action, identifiers, email_students, course_ke log.info("%s", error) errors.append(error) break - enroll_email(course_key, email, auto_enroll=True, message_students=email_students, message_params=email_params) + enroll_email( + course_key, + email, + auto_enroll=True, + message_students=email_students, + message_params=email_params + ) elif action == 'Unenroll' or action == 'revoke': # lint-amnesty, pylint: disable=consider-using-in for identifier in identifiers: try: diff --git a/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md b/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md deleted file mode 100644 index 4455ce9f3c55..000000000000 --- a/openedx/core/djangoapps/ace_common/docs/push_notifications_configuration.md +++ /dev/null @@ -1,63 +0,0 @@ -# Configure mobile push notifications in edx-platform - - -### 1. Create a new Firebase project - -All push notifications in Open edX are sent via FCM service to start with it you need to create -a new Firebase project in Firebase console https://console.firebase.google.com/ - -### 2. Provide service account credentials to initialize an FCM admin application in edx-platform - -To configure sending push notifications via FCM from edx-platform, you need to generate private -key for Firebase admin SDK in Project settings > Service accounts section. - -After downloading .json key, you should mount it to LMS/CMS containers and specify a path to -the mounted file using FIREBASE_CREDENTIALS_PATH settings -[variable](https://github.com/openedx/edx-platform/pull/34971/files#diff-f694c479e5c9b133241a799e1ddf33d5d5133bfdec91e3f7d371e094c9999e74R31). There is also an alternative option, -which is to add the value from the .json key to the FIREBASE_CREDENTIALS environment -[variable](https://github.com/openedx/edx-platform/pull/34971/files#diff-f694c479e5c9b133241a799e1ddf33d5d5133bfdec91e3f7d371e094c9999e74R34), -like a python dictionary. - -https://github.com/openedx/edx-ace/blob/master/docs/decisions/0002-push-notifications.rst?plain=1#L108 - - -### 3. Configure and build mobile applications - -Use the supported Open edX mobile applications: - -https://github.com/openedx/openedx-app-android/ - -https://github.com/openedx/openedx-app-ios - -#### 3.1 Configure oauth2 - -First you need to configure Oauth applications for each mobile client in edx-platform. You should create separate -entries for Android and IOS applications in the Django OAuth Toolkit > Applications. - -Fill in all required fields in the form: - - Client ID: . - - Client type: Public - - Authorization grant type: Resource owner password-based - - Public Client secret: - -Specify generated Client ID in mobile config.yaml file - -https://github.com/openedx/openedx-app-android/blob/main/Documentation/ConfigurationManagement.md#configuration-files - -https://github.com/openedx/openedx-app-ios/blob/main/Documentation/CONFIGURATION_MANAGEMENT.md#examples-of-config-files - -#### 3.2 Provide FCM credentials to the app - -Create new apps in Firebase Console for Android and IOS in Project settings > General section. - -Download credentials file, google-services.json for Android, or GoogleService-Info.plist for IOS. - -Copy/paste values from configuration file into config.yaml as shown in example configurations. - -https://github.com/openedx/openedx-app-android/blob/main/Documentation/ConfigurationManagement.md#configuration-files - -https://github.com/openedx/openedx-app-ios/blob/main/Documentation/CONFIGURATION_MANAGEMENT.md#examples-of-config-files - -Build applications and you’re ready to go! - From 13e3024ae3e62ed8c4f6ffb516822482f520870f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Fri, 6 Sep 2024 17:40:26 +0300 Subject: [PATCH 12/17] style: [FC-0047] fix code style issues --- lms/djangoapps/discussion/tasks.py | 3 +++ lms/djangoapps/instructor/views/api.py | 12 +++++++----- lms/djangoapps/mobile_api/notifications/urls.py | 5 ++--- lms/djangoapps/mobile_api/notifications/views.py | 2 +- openedx/core/djangoapps/ace_common/utils.py | 6 +----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lms/djangoapps/discussion/tasks.py b/lms/djangoapps/discussion/tasks.py index 65a03c687a49..da5a324ea77f 100644 --- a/lms/djangoapps/discussion/tasks.py +++ b/lms/djangoapps/discussion/tasks.py @@ -76,6 +76,9 @@ def __init__(self, *args, **kwargs): class CommentNotification(BaseMessageType): + """ + Notify discussion participants of new comments. + """ pass diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index d9a301b07e7f..990170d7b452 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -514,11 +514,13 @@ def post(self, request, course_id): # pylint: disable=too-many-statements reason='Enrolling via csv upload', state_transition=UNENROLLED_TO_ENROLLED, ) - enroll_email(course_id=course_id, - student_email=email, - auto_enroll=True, - email_students=notify_by_email, - email_params=email_params) + enroll_email( + course_id=course_id, + student_email=email, + auto_enroll=True, + message_students=notify_by_email, + message_params=email_params, + ) else: # update the course mode if already enrolled existing_enrollment = CourseEnrollment.get_enrollment(user, course_id) diff --git a/lms/djangoapps/mobile_api/notifications/urls.py b/lms/djangoapps/mobile_api/notifications/urls.py index b0fe46a86046..120fa39a975a 100644 --- a/lms/djangoapps/mobile_api/notifications/urls.py +++ b/lms/djangoapps/mobile_api/notifications/urls.py @@ -5,9 +5,8 @@ from .views import GCMDeviceViewSet -CREATE_GCM_DEVICE = GCMDeviceViewSet.as_view({'post': 'create'}) - +create_gcm_device_post_view = GCMDeviceViewSet.as_view({'post': 'create'}) urlpatterns = [ - path('create-token/', CREATE_GCM_DEVICE, name='gcmdevice-list'), + path('create-token/', create_gcm_device_post_view, name='gcmdevice-list'), ] diff --git a/lms/djangoapps/mobile_api/notifications/views.py b/lms/djangoapps/mobile_api/notifications/views.py index 4778dc83c99c..2621c2a3a2fb 100644 --- a/lms/djangoapps/mobile_api/notifications/views.py +++ b/lms/djangoapps/mobile_api/notifications/views.py @@ -30,7 +30,7 @@ class GCMDeviceViewSet(GCMDeviceViewSetBase): 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. + key/certificate access. Should be equal settings.FCM_APP_NAME. **Example Response** ```json { diff --git a/openedx/core/djangoapps/ace_common/utils.py b/openedx/core/djangoapps/ace_common/utils.py index 508ac4033cd1..7cf38c821976 100644 --- a/openedx/core/djangoapps/ace_common/utils.py +++ b/openedx/core/djangoapps/ace_common/utils.py @@ -10,11 +10,7 @@ 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 + import firebase_admin # pylint: disable=import-outside-toplevel if firebase_credentials: try: From eef99a6acc5547bd946c513fdb2a045aea7c51ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Fri, 6 Sep 2024 17:40:55 +0300 Subject: [PATCH 13/17] chore: [FC-0047] change push notifications texts --- .../discussion/edx_ace/responsenotification/push/body.txt | 2 +- lms/templates/instructor/edx_ace/allowedenroll/push/body.txt | 1 - .../instructor/edx_ace/allowedunenroll/push/body.txt | 1 - requirements/edx/base.txt | 2 -- requirements/edx/development.txt | 4 ---- requirements/edx/doc.txt | 2 -- requirements/edx/testing.txt | 2 -- 7 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt index c1fe3ba35b7f..ee97a6e329f5 100644 --- a/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt +++ b/lms/djangoapps/discussion/templates/discussion/edx_ace/responsenotification/push/body.txt @@ -1,2 +1,2 @@ {% load i18n %} -{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}{% endblocktrans %} +{% blocktrans trimmed %}{{ comment_username }} replied to {{ thread_title }}: {{ comment_body|truncatechars:200 }}{% endblocktrans %} diff --git a/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt b/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt index 14e4915f86e2..41ff994310e3 100644 --- a/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt +++ b/lms/templates/instructor/edx_ace/allowedenroll/push/body.txt @@ -1,5 +1,4 @@ {% 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 %} diff --git a/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt b/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt index b825ce1d4d18..c7342b6830b5 100644 --- a/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt +++ b/lms/templates/instructor/edx_ace/allowedunenroll/push/body.txt @@ -1,5 +1,4 @@ {% 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 %} diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 755e51d26fc8..85167e0d36ca 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -4,8 +4,6 @@ # # 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 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 9c6bbbc2bbf2..876bc4fcc4a2 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -4,10 +4,6 @@ # # 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 diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 971dc13b06cf..a222e78518ed 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -4,8 +4,6 @@ # # 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 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index 0d694458d1b8..ac468974eb6c 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -4,8 +4,6 @@ # # 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 From 069a52a27b16d44a42efa8f8fbf799f9222af496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=86=D0=B2=D0=B0=D0=BD=20=D0=9D=D1=94=D0=B4=D1=94=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=96=D1=86=D0=B5=D0=B2?= Date: Wed, 11 Sep 2024 15:39:37 +0300 Subject: [PATCH 14/17] style: [FC-0047] remove unnecessary pass --- lms/djangoapps/discussion/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/discussion/tasks.py b/lms/djangoapps/discussion/tasks.py index da5a324ea77f..3fef4f5f7cef 100644 --- a/lms/djangoapps/discussion/tasks.py +++ b/lms/djangoapps/discussion/tasks.py @@ -79,7 +79,6 @@ class CommentNotification(BaseMessageType): """ Notify discussion participants of new comments. """ - pass @shared_task(base=LoggedTask) From 8f88db2cad12cac6f7cc4b77c3bf2def6719486f Mon Sep 17 00:00:00 2001 From: Mohammad Ahtasham ul Hassan <60315450+aht007@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:14:47 +0500 Subject: [PATCH 15/17] feat: add course_run_key to learner home upgrade url (#35461) * fix: fix learner home URL to have course_run_key --- lms/djangoapps/learner_home/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/learner_home/serializers.py b/lms/djangoapps/learner_home/serializers.py index b3471715b9dc..3d156f3640ca 100644 --- a/lms/djangoapps/learner_home/serializers.py +++ b/lms/djangoapps/learner_home/serializers.py @@ -3,7 +3,7 @@ """ from datetime import date, timedelta -from urllib.parse import urljoin +from urllib.parse import urlencode, urljoin from django.conf import settings from django.urls import reverse @@ -132,7 +132,13 @@ def get_upgradeUrl(self, instance): ) if ecommerce_payment_page and verified_sku: - return f"{ecommerce_payment_page}?sku={verified_sku}" + query_params = { + 'sku': verified_sku, + 'course_run_key': str(instance.course_id) + } + encoded_params = urlencode(query_params) + upgrade_url = f"{ecommerce_payment_page}?{encoded_params}" + return upgrade_url def get_resumeUrl(self, instance): return self.context.get("resume_course_urls", {}).get(instance.course_id) From 46777610a4227589b0b36c6eb99da5a90815b04a Mon Sep 17 00:00:00 2001 From: Awais Qureshi Date: Fri, 20 Sep 2024 17:06:19 +0500 Subject: [PATCH 16/17] feat: upgrading simple api to drf compatible ( 17th ) (#35394) * feat: upgrading simple api to drf compatible. --- lms/djangoapps/instructor/tests/test_api.py | 10 +++ lms/djangoapps/instructor/views/api.py | 71 ++++++++++++------- lms/djangoapps/instructor/views/api_urls.py | 2 +- lms/djangoapps/instructor/views/serializer.py | 7 ++ 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index e8bcc81318da..51fc514c4879 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -4175,6 +4175,16 @@ def test_change_due_date_with_reason(self): # This operation regenerates the cache, so we can use cached results from edx-when. assert get_date_for_block(self.course, self.week1, self.user1, use_cached=True) == due_date + def test_reset_due_date_with_reason(self): + url = reverse('reset_due_date', kwargs={'course_id': str(self.course.id)}) + response = self.client.post(url, { + 'student': self.user1.username, + 'url': str(self.week1.location), + 'reason': 'Testing reason.' # this is optional field. + }) + assert response.status_code == 200 + assert 'Successfully reset due date for student' in response.content.decode('utf-8') + def test_change_to_invalid_due_date(self): url = reverse('change_due_date', kwargs={'course_id': str(self.course.id)}) response = self.client.post(url, { diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 58556ee9ab02..6978eaf3fe96 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -137,7 +137,6 @@ handle_dashboard_error, keep_field_private, parse_datetime, - require_student_from_identifier, set_due_date_extension, strip_if_string, ) @@ -3035,37 +3034,59 @@ def post(self, request, course_id): due_date.strftime('%Y-%m-%d %H:%M'))) -@handle_dashboard_error -@require_POST -@ensure_csrf_cookie -@cache_control(no_cache=True, no_store=True, must_revalidate=True) -@require_course_permission(permissions.GIVE_STUDENT_EXTENSION) -@require_post_params('student', 'url') -def reset_due_date(request, course_id): +@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch') +class ResetDueDate(APIView): """ Rescinds a due date extension for a student on a particular unit. """ - course = get_course_by_id(CourseKey.from_string(course_id)) - student = require_student_from_identifier(request.POST.get('student')) - unit = find_unit(course, request.POST.get('url')) - reason = strip_tags(request.POST.get('reason', '')) + permission_classes = (IsAuthenticated, permissions.InstructorPermission) + permission_name = permissions.GIVE_STUDENT_EXTENSION + serializer_class = BlockDueDateSerializer - version = getattr(course, 'course_version', None) + @method_decorator(ensure_csrf_cookie) + def post(self, request, course_id): + """ + reset a due date extension to a student for a particular unit. + params: + url (str): The URL related to the block that needs the due date update. + student (str): The email or username of the student whose access is being modified. + reason (str): Optional param. + """ + serializer_data = self.serializer_class(data=request.data, context={'disable_due_datetime': True}) + if not serializer_data.is_valid(): + return HttpResponseBadRequest(reason=serializer_data.errors) - original_due_date = get_date_for_block(course_id, unit.location, published_version=version) + student = serializer_data.validated_data.get('student') + if not student: + response_payload = { + 'error': f'Could not find student matching identifier: {request.data.get("student")}' + } + return JsonResponse(response_payload) - set_due_date_extension(course, unit, student, None, request.user, reason=reason) - if not original_due_date: - # It's possible the normal due date was deleted after an extension was granted: - return JsonResponse( - _("Successfully removed invalid due date extension (unit has no due date).") - ) + course = get_course_by_id(CourseKey.from_string(course_id)) + unit = find_unit(course, serializer_data.validated_data.get('url')) + reason = strip_tags(serializer_data.validated_data.get('reason', '')) + + version = getattr(course, 'course_version', None) + + original_due_date = get_date_for_block(course_id, unit.location, published_version=version) - original_due_date_str = original_due_date.strftime('%Y-%m-%d %H:%M') - return JsonResponse(_( - 'Successfully reset due date for student {0} for {1} ' - 'to {2}').format(student.profile.name, _display_unit(unit), - original_due_date_str)) + try: + set_due_date_extension(course, unit, student, None, request.user, reason=reason) + if not original_due_date: + # It's possible the normal due date was deleted after an extension was granted: + return JsonResponse( + _("Successfully removed invalid due date extension (unit has no due date).") + ) + + original_due_date_str = original_due_date.strftime('%Y-%m-%d %H:%M') + return JsonResponse(_( + 'Successfully reset due date for student {0} for {1} ' + 'to {2}').format(student.profile.name, _display_unit(unit), + original_due_date_str)) + + except Exception as error: # pylint: disable=broad-except + return JsonResponse({'error': str(error)}, status=400) @handle_dashboard_error diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 9c0939a1c1b8..a248b46ae531 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -51,7 +51,7 @@ path('update_forum_role_membership', api.update_forum_role_membership, name='update_forum_role_membership'), path('change_due_date', api.ChangeDueDate.as_view(), name='change_due_date'), path('send_email', api.SendEmail.as_view(), name='send_email'), - path('reset_due_date', api.reset_due_date, name='reset_due_date'), + path('reset_due_date', api.ResetDueDate.as_view(), name='reset_due_date'), path('show_unit_extensions', api.show_unit_extensions, name='show_unit_extensions'), path('show_student_extensions', api.ShowStudentExtensions.as_view(), name='show_student_extensions'), diff --git a/lms/djangoapps/instructor/views/serializer.py b/lms/djangoapps/instructor/views/serializer.py index da91eba43124..5d123ad66c81 100644 --- a/lms/djangoapps/instructor/views/serializer.py +++ b/lms/djangoapps/instructor/views/serializer.py @@ -215,3 +215,10 @@ def validate_student(self, value): return None return user + + def __init__(self, *args, **kwargs): + # Get context to check if `due_datetime` should be optional + disable_due_datetime = kwargs.get('context', {}).get('disable_due_datetime', False) + super().__init__(*args, **kwargs) + if disable_due_datetime: + self.fields['due_datetime'].required = False From 0f975adc14958c88e9448cf2feba5478e932f645 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Fri, 20 Sep 2024 08:51:51 -0400 Subject: [PATCH 17/17] feat: Be able to login to bare-metal studio easily. (#35172) * feat: Be able to login to bare-metal studio easily. Updating the documentation and the devstack.py files so that if you're running bare-metal you can easily setup studio login via the LMS. I also added the Ports that the various MFEs expect to the runserver scripts so that it's easier to run those locally as well. Co-authored-by: Kyle McCormick --- README.rst | 33 +++++++++++++++++++++++++++++++-- cms/envs/devstack.py | 3 ++- lms/envs/minimal.yml | 3 +++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index e74176faf91e..64186fca3ace 100644 --- a/README.rst +++ b/README.rst @@ -124,6 +124,35 @@ sites):: ./manage.py lms collectstatic ./manage.py cms collectstatic +Set up CMS SSO (for Development):: + + ./manage.py lms manage_user studio_worker example@example.com --unusable-password + # DO NOT DO THIS IN PRODUCTION. It will make your auth insecure. + ./manage.py lms create_dot_application studio-sso-id studio_worker \ + --grant-type authorization-code \ + --skip-authorization \ + --redirect-uris 'http://localhost:18010/complete/edx-oauth2/' \ + --scopes user_id \ + --client-id 'studio-sso-id' \ + --client-secret 'studio-sso-secret' + +Set up CMS SSO (for Production): + +* Create the CMS user and the OAuth application:: + + ./manage.py lms manage_user studio_worker --unusable-password + ./manage.py lms create_dot_application studio-sso-id studio_worker \ + --grant-type authorization-code \ + --skip-authorization \ + --redirect-uris 'http://localhost:18010/complete/edx-oauth2/' \ + --scopes user_id + +* Log into Django admin (eg. http://localhost:18000/admin/oauth2_provider/application/), + click into the application you created above (``studio-sso-id``), and copy its "Client secret". +* In your private LMS_CFG yaml file or your private Django settings module: + + * Set ``SOCIAL_AUTH_EDX_OAUTH2_KEY`` to the client ID (``studio-sso-id``). + * Set ``SOCIAL_AUTH_EDX_OAUTH2_SECRET`` to the client secret (which you copied). Run the Platform ---------------- @@ -131,11 +160,11 @@ First, ensure MySQL, Mongo, and Memcached are running. Start the LMS:: - ./manage.py lms runserver + ./manage.py lms runserver 18000 Start the CMS:: - ./manage.py cms runserver + ./manage.py cms runserver 18010 This will give you a mostly-headless Open edX platform. Most frontends have been migrated to "Micro-Frontends (MFEs)" which need to be installed and run diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index e944d67eda1b..1d3a510cdc4c 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -267,7 +267,8 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing ################ Using LMS SSO for login to Studio ################ SOCIAL_AUTH_EDX_OAUTH2_KEY = 'studio-sso-key' SOCIAL_AUTH_EDX_OAUTH2_SECRET = 'studio-sso-secret' # in stage, prod would be high-entropy secret -SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = 'http://edx.devstack.lms:18000' # routed internally server-to-server +# routed internally server-to-server +SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT = ENV_TOKENS.get('SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT', 'http://edx.devstack.lms:18000') SOCIAL_AUTH_EDX_OAUTH2_PUBLIC_URL_ROOT = 'http://localhost:18000' # used in browser redirect # Don't form the return redirect URL with HTTPS on devstack diff --git a/lms/envs/minimal.yml b/lms/envs/minimal.yml index d455d1f3dbf8..51d7bbf499c4 100644 --- a/lms/envs/minimal.yml +++ b/lms/envs/minimal.yml @@ -36,3 +36,6 @@ LMS_INTERNAL_ROOT_URL: "http://localhost" # So that Swagger config code doesn't complain API_ACCESS_MANAGER_EMAIL: "api-access@example.com" + +# So that you can login to studio on bare-metal +SOCIAL_AUTH_EDX_OAUTH2_URL_ROOT: 'http://localhost:18000'