Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: [AXM-425] add docs for configure mobile push notifications #2589

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
49f9f71
feat: [AXM-24] Update structure for course enrollments API (#2515)
KyryloKireiev Mar 19, 2024
626fe7b
feat: [AXM-47] Add course_status field to primary object (#2517)
KyryloKireiev Mar 22, 2024
60a166f
feat: [AXM-40] add courses progress to enrollment endpoint (#2519)
NiedielnitsevIvan Mar 22, 2024
aadc659
feat: [AXM-53] add assertions for primary course (#2522)
NiedielnitsevIvan Apr 2, 2024
172f34c
feat: [AXM-200] Implement user's enrolments status API (#2530)
KyryloKireiev Apr 8, 2024
c5281db
feat: [AXM-33] create enrollments filtering by course completion stat…
NiedielnitsevIvan Apr 8, 2024
95e1dd7
feat: [AXM-236] Add progress for other courses (#2536)
NiedielnitsevIvan Apr 10, 2024
d434c5f
fix: [AXM-277] Change _get_last_visited_block_path_and_unit_name meth…
KyryloKireiev Apr 16, 2024
9964dad
feat: [AXM-297] Add progress to assignments in BlocksInfoInCourseView…
KyryloKireiev Apr 25, 2024
4f5a04e
feat: [AXM-288] Change response to represent Future assignments the s…
KyryloKireiev Apr 29, 2024
81ff17b
feat: [AXM-252] add settings for edx-ace push notifications (#2541)
NiedielnitsevIvan Apr 29, 2024
4ad6516
feat: [AXM-271] Add push notification event to discussions (#2548)
NiedielnitsevIvan Apr 29, 2024
3a8dd5e
feat: [AXM-287,310,331] Change course progress calculation logic (#2553)
KyryloKireiev May 13, 2024
0265c30
feat: [AXM-373] Add push notification event about course invitations …
NiedielnitsevIvan May 14, 2024
659eed4
refactor: [AXM-475] refactor firebase settings (#2560)
NiedielnitsevIvan May 22, 2024
579e726
feat: [AXM-556] refactor discussion push notifications sending
NiedielnitsevIvan May 29, 2024
7dd5a4c
fix: fix typo
NiedielnitsevIvan May 29, 2024
05a9c8c
test: [AXM-556] add topic_id to tests
NiedielnitsevIvan May 29, 2024
24ebc78
feat: [AXM-571] change texts in the push notifications (#2569)
NiedielnitsevIvan Jun 5, 2024
2c74826
feat: [AXM-542] create xblock renderer
NiedielnitsevIvan Jun 3, 2024
abcef5e
feat: [AXM-349] Implement media generation for problem xblock (#2568)
NiedielnitsevIvan Jun 11, 2024
315a232
feat: [AXM-355] add information about offline metadata to course blocks
NiedielnitsevIvan Jun 6, 2024
5875b50
feat: [AXM-355] added a mechanism for generating offline content of b…
NiedielnitsevIvan Jun 6, 2024
3613e7d
style: [AXM-355] fix indentation
NiedielnitsevIvan Jun 7, 2024
a50ce23
refactor: [AXM-583] refactor offline content generation
NiedielnitsevIvan Jun 10, 2024
0d3c27b
feat: [AXM-343] Transfer offline download solution for HTML xblock
NiedielnitsevIvan Jun 10, 2024
c3bf3cc
feat: [AXM-589] Add retry to content generation task (#2574)
KyryloKireiev Jun 13, 2024
8f0a187
feat: [AXM-334] Allow easily adding Firebase credentials in Tutor (#2…
KyryloKireiev Jun 14, 2024
4a5ad34
feat: [AXM-644] Add authorization via cms worker for content generati…
NiedielnitsevIvan Jun 13, 2024
6a3ac0a
feat: [AXM-644] add tests for content generation view
NiedielnitsevIvan Jun 13, 2024
d0cc126
fix: [AXM-644] add problem type to supported types
NiedielnitsevIvan Jun 13, 2024
9b2c961
style: [AXM-644] add missing docstrings
NiedielnitsevIvan Jun 17, 2024
e6f5ac2
fix: fix problem block offline generations
NiedielnitsevIvan Jun 17, 2024
02c2df2
fix: add logs for 404 error on gettings xblock
NiedielnitsevIvan Jun 17, 2024
337ec80
fix: fix xblock close checking for HTML xblock
NiedielnitsevIvan Jun 17, 2024
d3a7f86
fix: [AXM-748] fix after review
NiedielnitsevIvan Jun 19, 2024
04d79c8
feat: [AXM-749] Implement s3 storage supporting (#2581)
KyryloKireiev Jun 20, 2024
3e076fc
fix: [AXM-791] fix error 404 handling and optimize for not modified b…
NiedielnitsevIvan Jun 21, 2024
c2a7bee
refactor: [AXM-361] refactor JS bridge for IOS and Android (#2580)
NiedielnitsevIvan Jun 22, 2024
bcc957e
feat: [AXM-755] save external files and fonts to offline block conten…
NiedielnitsevIvan Jun 22, 2024
6fa315c
fix: don't display icon in offline mode
GlugovGrGlib Jun 22, 2024
cf5694e
test: [AXM-636] Cover Offline Mode API with unit tests (#2577)
KyryloKireiev Jun 24, 2024
ea45958
fix: encode spaces as %20
GlugovGrGlib Jun 26, 2024
0a7a536
fix: pass the full options message to mobile bridge
GlugovGrGlib Jun 26, 2024
8e29273
refactor: update path to store blocks
GlugovGrGlib Jun 26, 2024
ba44ee9
docs: [AXM-425] add docs for configure mobile push notifications
NiedielnitsevIvan Jul 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cms/djangoapps/contentstore/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timezone
from functools import wraps
from typing import Optional
from urllib.parse import urljoin

from django.conf import settings
from django.core.cache import cache
Expand All @@ -21,19 +22,22 @@
CoursewareSearchIndexer,
LibrarySearchIndexer,
)
from cms.djangoapps.contentstore.utils import get_cms_api_client
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from common.djangoapps.util.block_utils import yield_dynamic_block_descendants
from lms.djangoapps.grades.api import task_compute_all_grades_for_course
from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines
from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course_task
from openedx.core.lib.gating import api as gating_api
from openedx.features.offline_mode.toggles import is_offline_mode_enabled
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, modulestore
from .signals import GRADING_POLICY_CHANGED

log = logging.getLogger(__name__)

GRADING_POLICY_COUNTDOWN_SECONDS = 3600
LMS_OFFLINE_HANDLER_URL = '/offline_mode/handle_course_published'


def locked(expiry_seconds, key): # lint-amnesty, pylint: disable=missing-function-docstring
Expand Down Expand Up @@ -155,6 +159,14 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
# Send to a signal for catalog info changes as well, but only once we know the transaction is committed.
transaction.on_commit(lambda: emit_catalog_info_changed_signal(course_key))

if is_offline_mode_enabled(course_key):
client = get_cms_api_client()
client.post(
url=urljoin(settings.LMS_ROOT_URL, LMS_OFFLINE_HANDLER_URL),
data={'course_id': str(course_key)},
)
log.info('Sent course_published event to offline mode handler')


@receiver(SignalHandler.course_deleted)
def listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
Expand Down
17 changes: 17 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
import configparser
import logging
import re
import requests
from collections import defaultdict
from contextlib import contextmanager
from datetime import datetime, timezone
from urllib.parse import quote_plus
from uuid import uuid4

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.utils import translation
from django.utils.translation import gettext as _
from edx_rest_api_client.auth import SuppliedJwtAuth
from eventtracking import tracker
from help_tokens.core import HelpUrlExpert
from lti_consumer.models import CourseAllowPIISharingInLTIFlag
Expand Down Expand Up @@ -67,6 +70,7 @@
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.site_configuration.models import SiteConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.html_to_text import html_to_text
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
Expand Down Expand Up @@ -107,6 +111,7 @@

IMPORTABLE_FILE_TYPES = ('.tar.gz', '.zip')
log = logging.getLogger(__name__)
User = get_user_model()


def add_instructor(course_key, requesting_user, new_instructor):
Expand Down Expand Up @@ -2317,3 +2322,15 @@ def get_xblock_render_context(request, block):
return str(exc)

return ""


def get_cms_api_client():
"""
Returns an API client which can be used to make requests from the CMS service.
"""
user = User.objects.get(username=settings.CMS_SERVICE_USER_NAME)
jwt = create_jwt_for_user(user)
client = requests.Session()
client.auth = SuppliedJwtAuth(jwt)

return client
2 changes: 2 additions & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2525,6 +2525,8 @@
EXAMS_SERVICE_URL = 'http://localhost:18740/api/v1'
EXAMS_SERVICE_USERNAME = 'edx_exams_worker'

CMS_SERVICE_USER_NAME = 'edxapp_cms_worker'

FINANCIAL_REPORTS = {
'STORAGE_TYPE': 'localfs',
'BUCKET': None,
Expand Down
3 changes: 3 additions & 0 deletions cms/envs/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def get_env_setting(setting):
AUTHORING_API_URL = ENV_TOKENS.get('AUTHORING_API_URL', '')
# Note that FEATURES['PREVIEW_LMS_BASE'] gets read in from the environment file.

CMS_SERVICE_USER_NAME = ENV_TOKENS.get('CMS_SERVICE_USER_NAME', CMS_SERVICE_USER_NAME)


OPENAI_API_KEY = ENV_TOKENS.get('OPENAI_API_KEY', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT = ENV_TOKENS.get('LEARNER_ENGAGEMENT_PROMPT_FOR_ACTIVE_CONTRACT', '')
LEARNER_ENGAGEMENT_PROMPT_FOR_NON_ACTIVE_CONTRACT = ENV_TOKENS.get(
Expand Down
60 changes: 60 additions & 0 deletions common/djangoapps/student/models/course_enrollment.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,71 @@ class UnenrollmentNotAllowed(CourseEnrollmentException):
pass


class CourseEnrollmentQuerySet(models.QuerySet):
"""
Custom queryset for CourseEnrollment with Table-level filter methods.
"""

def active(self):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently active.
"""
return self.filter(is_active=True)

def without_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that do not have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.exclude(course_id__in=course_ids_with_certificates)

def with_certificates(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have a certificate.
"""
from lms.djangoapps.certificates.models import GeneratedCertificate # pylint: disable=import-outside-toplevel
course_ids_with_certificates = GeneratedCertificate.objects.filter(
user__username=user_username
).values_list('course_id', flat=True)
return self.filter(course_id__in=course_ids_with_certificates)

def in_progress(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that are currently in progress.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(
Q(course__start__lte=now, course__end__gte=now)
| Q(course__start__isnull=True, course__end__isnull=True)
| Q(course__start__isnull=True, course__end__gte=now)
| Q(course__start__lte=now, course__end__isnull=True),
)

def completed(self, user_username):
"""
Returns a queryset of CourseEnrollment objects for courses that have been completed.
"""
return self.active().with_certificates(user_username)

def expired(self, user_username, time_zone=UTC):
"""
Returns a queryset of CourseEnrollment objects for courses that have expired.
"""
now = datetime.now(time_zone)
return self.active().without_certificates(user_username).filter(course__end__lt=now)


class CourseEnrollmentManager(models.Manager):
"""
Custom manager for CourseEnrollment with Table-level filter methods.
"""

def get_queryset(self):
return CourseEnrollmentQuerySet(self.model, using=self._db)

def is_small_course(self, course_id):
"""
Returns false if the number of enrollments are one greater than 'max_enrollments' else true
Expand Down
8 changes: 5 additions & 3 deletions lms/djangoapps/course_api/blocks/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from datetime import datetime
from unittest import mock
from unittest.mock import Mock
from unittest.mock import MagicMock, Mock
from urllib.parse import urlencode, urlunparse

import ddt
Expand Down Expand Up @@ -209,8 +209,9 @@ def test_not_authenticated_public_course_with_all_blocks(self):
self.query_params['all_blocks'] = True
self.verify_response(403)

@mock.patch('lms.djangoapps.mobile_api.course_info.serializers.get_course_assignments', return_value=[])
@mock.patch("lms.djangoapps.course_api.blocks.forms.permissions.is_course_public", Mock(return_value=True))
def test_not_authenticated_public_course_with_blank_username(self):
def test_not_authenticated_public_course_with_blank_username(self, get_course_assignment_mock: MagicMock) -> None:
"""
Verify behaviour when accessing course blocks of a public course for anonymous user anonymously.
"""
Expand Down Expand Up @@ -368,7 +369,8 @@ def test_extra_field_when_not_requested(self):
block_data['type'] == 'course'
)

def test_data_researcher_access(self):
@mock.patch('lms.djangoapps.mobile_api.course_info.serializers.get_course_assignments', return_value=[])
def test_data_researcher_access(self, get_course_assignment_mock: MagicMock) -> None:
"""
Test if data researcher has access to the api endpoint
"""
Expand Down
8 changes: 6 additions & 2 deletions lms/djangoapps/courseware/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import logging
from collections import defaultdict, namedtuple
from datetime import datetime
from datetime import datetime, timedelta

import six
import pytz
Expand Down Expand Up @@ -587,7 +587,7 @@ def get_course_blocks_completion_summary(course_key, user):


@request_cached()
def get_course_assignments(course_key, user, include_access=False): # lint-amnesty, pylint: disable=too-many-statements
def get_course_assignments(course_key, user, include_access=False, include_without_due=False,): # lint-amnesty, pylint: disable=too-many-statements
"""
Returns a list of assignment (at the subsection/sequential level) due dates for the given course.

Expand All @@ -607,6 +607,10 @@ def get_course_assignments(course_key, user, include_access=False): # lint-amne
for subsection_key in block_data.get_children(section_key):
due = block_data.get_xblock_field(subsection_key, 'due')
graded = block_data.get_xblock_field(subsection_key, 'graded', False)

if not due and include_without_due:
due = now + timedelta(days=1000)

if due and graded:
first_component_block_id = get_first_component_of_block(subsection_key, block_data)
contains_gated_content = include_access and block_data.get_xblock_field(
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/discussion/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ def create_message_context(comment, site):
'course_id': str(thread.course_id),
'comment_id': comment.id,
'comment_body': comment.body,
'comment_body_text': comment.body_text,
'comment_author_id': comment.user_id,
'comment_created_at': comment.created_at, # comment_client models dates are already serialized
'comment_parent_id': comment.parent_id,
'thread_id': thread.id,
'thread_title': thread.title,
'thread_author_id': thread.user_id,
Expand Down
71 changes: 63 additions & 8 deletions lms/djangoapps/discussion/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.contrib.sites.models import Site
from edx_ace import ace
from edx_ace.channel import ChannelType
from edx_ace.recipient import Recipient
from edx_ace.utils import date
from edx_django_utils.monitoring import set_code_owner_attribute
Expand Down Expand Up @@ -74,6 +75,12 @@ def __init__(self, *args, **kwargs):
self.options['transactional'] = True


class CommentNotification(BaseMessageType):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.options['transactional'] = True


@shared_task(base=LoggedTask)
@set_code_owner_attribute
def send_ace_message(context): # lint-amnesty, pylint: disable=missing-function-docstring
Expand All @@ -82,17 +89,40 @@ def send_ace_message(context): # lint-amnesty, pylint: disable=missing-function
if _should_send_message(context):
context['site'] = Site.objects.get(id=context['site_id'])
thread_author = User.objects.get(id=context['thread_author_id'])
with emulate_http_request(site=context['site'], user=thread_author):
message_context = _build_message_context(context)
comment_author = User.objects.get(id=context['comment_author_id'])
with emulate_http_request(site=context['site'], user=comment_author):
message_context = _build_message_context(context, notification_type='forum_response')
message = ResponseNotification().personalize(
Recipient(thread_author.id, thread_author.email),
_get_course_language(context['course_id']),
message_context
)
log.info('Sending forum comment email notification with context %s', message_context)
ace.send(message)
log.info('Sending forum comment notification with context %s', message_context)
if _is_first_comment(context['comment_id'], context['thread_id']):
limit_to_channels = None
else:
limit_to_channels = [ChannelType.PUSH]
ace.send(message, limit_to_channels=limit_to_channels)
_track_notification_sent(message, context)

elif _should_send_subcomment_message(context):
context['site'] = Site.objects.get(id=context['site_id'])
comment_author = User.objects.get(id=context['comment_author_id'])
thread_author = User.objects.get(id=context['thread_author_id'])

with emulate_http_request(site=context['site'], user=comment_author):
message_context = _build_message_context(context)
message = CommentNotification().personalize(
Recipient(thread_author.id, thread_author.email),
_get_course_language(context['course_id']),
message_context
)
log.info('Sending forum comment notification with context %s', message_context)
ace.send(message, limit_to_channels=[ChannelType.PUSH])
_track_notification_sent(message, context)
else:
return


@shared_task(base=LoggedTask)
@set_code_owner_attribute
Expand Down Expand Up @@ -154,19 +184,36 @@ def _should_send_message(context):
return (
_is_user_subscribed_to_thread(cc_thread_author, context['thread_id']) and
_is_not_subcomment(context['comment_id']) and
_is_first_comment(context['comment_id'], context['thread_id'])
not _comment_author_is_thread_author(context)
)


def _should_send_subcomment_message(context):
cc_thread_author = cc.User(id=context['thread_author_id'], course_id=context['course_id'])
return (
_is_user_subscribed_to_thread(cc_thread_author, context['thread_id']) and
_is_subcomment(context['comment_id']) and
not _comment_author_is_thread_author(context)
)


def _comment_author_is_thread_author(context):
return context.get('comment_author_id', '') == context['thread_author_id']


def _is_content_still_reported(context):
if context.get('comment_id') is not None:
return len(cc.Comment.find(context['comment_id']).abuse_flaggers) > 0
return len(cc.Thread.find(context['thread_id']).abuse_flaggers) > 0


def _is_not_subcomment(comment_id):
def _is_subcomment(comment_id):
comment = cc.Comment.find(id=comment_id).retrieve()
return not getattr(comment, 'parent_id', None)
return getattr(comment, 'parent_id', None)


def _is_not_subcomment(comment_id):
return not _is_subcomment(comment_id)


def _is_first_comment(comment_id, thread_id): # lint-amnesty, pylint: disable=missing-function-docstring
Expand Down Expand Up @@ -204,7 +251,7 @@ def _get_course_language(course_id):
return language


def _build_message_context(context): # lint-amnesty, pylint: disable=missing-function-docstring
def _build_message_context(context, notification_type='forum_comment'): # lint-amnesty, pylint: disable=missing-function-docstring
message_context = get_base_template_context(context['site'])
message_context.update(context)
thread_author = User.objects.get(id=context['thread_author_id'])
Expand All @@ -218,6 +265,14 @@ def _build_message_context(context): # lint-amnesty, pylint: disable=missing-fu
'thread_username': thread_author.username,
'comment_username': comment_author.username,
'post_link': post_link,
'push_notification_extra_context': {
'course_id': str(context['course_id']),
'parent_id': str(context['comment_parent_id']),
'notification_type': notification_type,
'topic_id': str(context['thread_commentable_id']),
'thread_id': context['thread_id'],
'comment_id': context['comment_id'],
},
'comment_created_at': date.deserialize(context['comment_created_at']),
'thread_created_at': date.deserialize(context['thread_created_at'])
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}
{% blocktrans trimmed %}{{ comment_username }} commented to {{ thread_title }}:{% endblocktrans %}
{{ comment_body_text }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load i18n %}

{% blocktrans %}Comment to {{ thread_title }}{% endblocktrans %}
Loading
Loading