* feat: Adds discussions settings for new discusions experience This commit adds new discussions settings for the new discussions experience. These are stored in the course so they can be a part of course import/export flow. These are also added to the discussions configuraiton API to allow MFEs to update the settings. The discussions API is currently available via LMS, however that means it cannot save changes to the modulestore. This also adds the API to the studio config so it can now also be accessed from studio and be used to save course settings. * fix: tests
181 lines
7.2 KiB
Python
181 lines
7.2 KiB
Python
"""
|
|
Defines asynchronous celery task for sending email notification (through edx-ace)
|
|
pertaining to new discussion forum comments.
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
from celery import shared_task
|
|
from celery_utils.logged_task import LoggedTask
|
|
from django.conf import settings # lint-amnesty, pylint: disable=unused-import
|
|
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
|
from django.contrib.sites.models import Site
|
|
from edx_ace import ace
|
|
from edx_ace.recipient import Recipient
|
|
from edx_ace.utils import date
|
|
from edx_django_utils.monitoring import set_code_owner_attribute
|
|
from eventtracking import tracker
|
|
from opaque_keys.edx.keys import CourseKey
|
|
from six.moves.urllib.parse import urljoin
|
|
|
|
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
|
from common.djangoapps.track import segment
|
|
from lms.djangoapps.discussion.django_comment_client.utils import (
|
|
permalink
|
|
)
|
|
from openedx.core.djangoapps.discussions.utils import get_accessible_discussion_xblocks_by_course_id
|
|
from openedx.core.djangoapps.ace_common.message import BaseMessageType
|
|
from openedx.core.djangoapps.ace_common.template_context import get_base_template_context
|
|
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
|
from openedx.core.djangoapps.django_comment_common.models import DiscussionsIdMapping
|
|
from openedx.core.lib.celery.task_utils import emulate_http_request
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
DEFAULT_LANGUAGE = 'en'
|
|
|
|
|
|
@shared_task(base=LoggedTask)
|
|
@set_code_owner_attribute
|
|
def update_discussions_map(context):
|
|
"""
|
|
Updates the mapping between discussion_id to discussion block usage key
|
|
for all discussion blocks in the given course.
|
|
|
|
context is a dict that contains:
|
|
course_id (string): identifier of the course
|
|
"""
|
|
course_key = CourseKey.from_string(context['course_id'])
|
|
discussion_blocks = get_accessible_discussion_xblocks_by_course_id(course_key, include_all=True)
|
|
discussions_id_map = {
|
|
discussion_block.discussion_id: str(discussion_block.location)
|
|
for discussion_block in discussion_blocks
|
|
}
|
|
DiscussionsIdMapping.update_mapping(course_key, discussions_id_map)
|
|
|
|
|
|
class ResponseNotification(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
|
|
context['course_id'] = CourseKey.from_string(context['course_id'])
|
|
|
|
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)
|
|
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)
|
|
_track_notification_sent(message, context)
|
|
|
|
|
|
def _track_notification_sent(message, context):
|
|
"""
|
|
Send analytics event for a sent email
|
|
"""
|
|
properties = {
|
|
'app_label': 'discussion',
|
|
'name': 'responsenotification', # This is 'Campaign' in GA
|
|
'language': message.language,
|
|
'uuid': str(message.uuid),
|
|
'send_uuid': str(message.send_uuid),
|
|
'thread_id': context['thread_id'],
|
|
'course_id': str(context['course_id']),
|
|
'thread_created_at': date.deserialize(context['thread_created_at']),
|
|
'nonInteraction': 1,
|
|
}
|
|
tracking_context = {
|
|
'host': context['site'].domain,
|
|
'path': '/', # make up a value, in order to allow the host to be passed along.
|
|
}
|
|
# The event used to specify the user_id as being the recipient of the email (i.e. the thread_author_id).
|
|
# This has the effect of interrupting the actual chain of events for that author, if any, while the
|
|
# email-sent event should really be associated with the sender, since that is what triggers the event.
|
|
with tracker.get_tracker().context(properties['app_label'], tracking_context):
|
|
segment.track(
|
|
user_id=context['thread_author_id'],
|
|
event_name='edx.bi.email.sent',
|
|
properties=properties
|
|
)
|
|
|
|
|
|
def _should_send_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_not_subcomment(context['comment_id']) and
|
|
_is_first_comment(context['comment_id'], context['thread_id'])
|
|
)
|
|
|
|
|
|
def _is_not_subcomment(comment_id):
|
|
comment = cc.Comment.find(id=comment_id).retrieve()
|
|
return not getattr(comment, 'parent_id', None)
|
|
|
|
|
|
def _is_first_comment(comment_id, thread_id): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
thread = cc.Thread.find(id=thread_id).retrieve(with_responses=True)
|
|
if getattr(thread, 'children', None):
|
|
first_comment = thread.children[0]
|
|
return first_comment.get('id') == comment_id
|
|
else:
|
|
return False
|
|
|
|
|
|
def _is_user_subscribed_to_thread(cc_user, thread_id): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
paginated_result = cc_user.subscribed_threads()
|
|
thread_ids = {thread['id'] for thread in paginated_result.collection}
|
|
|
|
while paginated_result.page < paginated_result.num_pages:
|
|
next_page = paginated_result.page + 1
|
|
paginated_result = cc_user.subscribed_threads(query_params={'page': next_page})
|
|
thread_ids.update(thread['id'] for thread in paginated_result.collection)
|
|
|
|
return thread_id in thread_ids
|
|
|
|
|
|
def _get_course_language(course_id):
|
|
course_overview = CourseOverview.objects.get(id=course_id)
|
|
language = course_overview.language or DEFAULT_LANGUAGE
|
|
return language
|
|
|
|
|
|
def _build_message_context(context): # 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'])
|
|
comment_author = User.objects.get(id=context['comment_author_id'])
|
|
message_context.update({
|
|
'thread_username': thread_author.username,
|
|
'comment_username': comment_author.username,
|
|
'post_link': _get_thread_url(context),
|
|
'comment_created_at': date.deserialize(context['comment_created_at']),
|
|
'thread_created_at': date.deserialize(context['thread_created_at'])
|
|
})
|
|
return message_context
|
|
|
|
|
|
def _get_thread_url(context): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
scheme = 'https' if settings.HTTPS == 'on' else 'http'
|
|
base_url = '{}://{}'.format(scheme, context['site'].domain)
|
|
thread_content = {
|
|
'type': 'thread',
|
|
'course_id': context['course_id'],
|
|
'commentable_id': context['thread_commentable_id'],
|
|
'id': context['thread_id'],
|
|
}
|
|
return urljoin(base_url, permalink(thread_content))
|