feat: send email to moderators after content is reported
This commit is contained in:
@@ -35,13 +35,14 @@ from openedx.core.djangoapps.discussions.utils import (
|
||||
get_group_names_by_id,
|
||||
has_required_keys,
|
||||
)
|
||||
import openedx.core.djangoapps.django_comment_common.comment_client as cc
|
||||
from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_COMMUNITY_TA,
|
||||
FORUM_ROLE_STUDENT,
|
||||
CourseDiscussionSettings,
|
||||
DiscussionsIdMapping,
|
||||
Role
|
||||
)
|
||||
Role,
|
||||
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_GROUP_MODERATOR)
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from openedx.core.lib.courses import get_course_by_id
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -146,6 +147,40 @@ def is_user_community_ta(user, course_id):
|
||||
return has_forum_access(user, course_id, FORUM_ROLE_COMMUNITY_TA)
|
||||
|
||||
|
||||
def get_users_with_roles(roles, course_id):
|
||||
"""
|
||||
Get all users with specified roles for a course
|
||||
"""
|
||||
users_with_roles = [
|
||||
user
|
||||
for role in Role.objects.filter(
|
||||
name__in=roles,
|
||||
course_id=course_id
|
||||
)
|
||||
for user in role.users.all()
|
||||
]
|
||||
return users_with_roles
|
||||
|
||||
|
||||
def get_users_with_moderator_roles(context):
|
||||
"""
|
||||
Get all users within the course with moderator roles
|
||||
"""
|
||||
moderators = get_users_with_roles([FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR,
|
||||
FORUM_ROLE_COMMUNITY_TA], context['course_id'])
|
||||
|
||||
context_thread = cc.Thread.find(context['thread_id'])
|
||||
if getattr(context_thread, 'group_id', None) is not None:
|
||||
group_moderators = get_users_with_roles([FORUM_ROLE_GROUP_MODERATOR], context['course_id'])
|
||||
course_discussion_settings = CourseDiscussionSettings.get(context['course_id'])
|
||||
moderators_in_group = [user for user in group_moderators if get_group_id_for_user(
|
||||
user, course_discussion_settings) == context_thread.group_id]
|
||||
moderators += moderators_in_group
|
||||
|
||||
moderators = set(moderators)
|
||||
return moderators
|
||||
|
||||
|
||||
def get_discussion_id_map_entry(xblock):
|
||||
"""
|
||||
Returns a tuple of (discussion_id, metadata) suitable for inclusion in the results of get_discussion_id_map().
|
||||
|
||||
@@ -27,6 +27,7 @@ from lms.djangoapps.course_blocks.api import get_course_blocks
|
||||
from lms.djangoapps.courseware.courses import get_course_with_access
|
||||
from lms.djangoapps.courseware.exceptions import CourseAccessRedirect
|
||||
from lms.djangoapps.discussion.toggles import ENABLE_LEARNERS_TAB_IN_DISCUSSIONS_MFE
|
||||
from lms.djangoapps.discussion.toggles_utils import reported_content_email_notification_enabled
|
||||
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, DiscussionTopicLink, Provider
|
||||
from openedx.core.djangoapps.discussions.utils import get_accessible_discussion_xblocks
|
||||
from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment
|
||||
@@ -51,6 +52,8 @@ from openedx.core.djangoapps.django_comment_common.signals import (
|
||||
thread_deleted,
|
||||
thread_edited,
|
||||
thread_voted,
|
||||
thread_flagged,
|
||||
comment_flagged,
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError, DiscussionNotFoundError, PageNotFoundError
|
||||
@@ -1018,6 +1021,11 @@ def _handle_abuse_flagged_field(form_value, user, cc_content):
|
||||
"""mark or unmark thread/comment as abused"""
|
||||
if form_value:
|
||||
cc_content.flagAbuse(user, cc_content)
|
||||
if reported_content_email_notification_enabled(CourseKey.from_string(cc_content.course_id)):
|
||||
if cc_content.type == 'thread':
|
||||
thread_flagged.send(sender='flag_abuse_for_thread', user=user, post=cc_content)
|
||||
else:
|
||||
comment_flagged.send(sender='flag_abuse_for_comment', user=user, post=cc_content)
|
||||
else:
|
||||
cc_content.unFlagAbuse(user, cc_content, removeAll=False)
|
||||
|
||||
|
||||
@@ -60,9 +60,31 @@ def send_discussion_email_notification(sender, user, post, **kwargs): # lint-am
|
||||
send_message(post, current_site)
|
||||
|
||||
|
||||
def send_message(comment, site): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
@receiver(signals.comment_flagged)
|
||||
@receiver(signals.thread_flagged)
|
||||
def send_reported_content_email_notification(sender, user, post, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument
|
||||
current_site = get_current_site()
|
||||
if current_site is None:
|
||||
log.info('Discussion: No current site, not sending notification about post: %s.', post.id)
|
||||
return
|
||||
|
||||
try:
|
||||
if not current_site.configuration.get_value(ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY, False):
|
||||
log_message = 'Discussion: reported content notifications not enabled for site: %s. ' \
|
||||
'Not sending message about post: %s.'
|
||||
log.info(log_message, current_site, post.id)
|
||||
return
|
||||
except SiteConfiguration.DoesNotExist:
|
||||
log_message = 'Discussion: No SiteConfiguration for site %s. Not sending message about post: %s.'
|
||||
log.info(log_message, current_site, post.id)
|
||||
return
|
||||
|
||||
send_message_for_reported_content(user, post, current_site, sender)
|
||||
|
||||
|
||||
def create_message_context(comment, site):
|
||||
thread = comment.thread
|
||||
context = {
|
||||
return {
|
||||
'course_id': str(thread.course_id),
|
||||
'comment_id': comment.id,
|
||||
'comment_body': comment.body,
|
||||
@@ -75,4 +97,31 @@ def send_message(comment, site): # lint-amnesty, pylint: disable=missing-functi
|
||||
'thread_commentable_id': thread.commentable_id,
|
||||
'site_id': site.id
|
||||
}
|
||||
|
||||
|
||||
def create_message_context_for_reported_content(user, post, site, sender):
|
||||
"""
|
||||
Create message context for reported content.
|
||||
"""
|
||||
context = {
|
||||
'user_id': user.id,
|
||||
'course_id': str(post.course_id),
|
||||
'thread_id': post.thread.id if sender == 'flag_abuse_for_comment' else post.id,
|
||||
'title': post.thread.title if sender == 'flag_abuse_for_comment' else post.title,
|
||||
'content_type': post.type,
|
||||
'comment_body': post.body,
|
||||
'thread_created_at': post.created_at,
|
||||
'thread_commentable_id': post.commentable_id,
|
||||
'site_id': site.id,
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
def send_message(comment, site): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
context = create_message_context(comment, site)
|
||||
tasks.send_ace_message.apply_async(args=[context])
|
||||
|
||||
|
||||
def send_message_for_reported_content(user, post, site, sender): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
context = create_message_context_for_reported_content(user, post, site, sender)
|
||||
tasks.send_ace_message_for_reported_content.apply_async(args=[context], countdown=120)
|
||||
|
||||
@@ -21,8 +21,10 @@ 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 common.lib.xmodule.xmodule.modulestore.django import modulestore
|
||||
from lms.djangoapps.discussion.django_comment_client.utils import (
|
||||
permalink
|
||||
permalink,
|
||||
get_users_with_moderator_roles,
|
||||
)
|
||||
from openedx.core.djangoapps.discussions.utils import get_accessible_discussion_xblocks_by_course_id
|
||||
from openedx.core.djangoapps.ace_common.message import BaseMessageType
|
||||
@@ -62,6 +64,12 @@ class ResponseNotification(BaseMessageType):
|
||||
self.options['transactional'] = True
|
||||
|
||||
|
||||
class ReportedContentNotification(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,6 +90,31 @@ def send_ace_message(context): # lint-amnesty, pylint: disable=missing-function
|
||||
_track_notification_sent(message, context)
|
||||
|
||||
|
||||
@shared_task(base=LoggedTask)
|
||||
@set_code_owner_attribute
|
||||
def send_ace_message_for_reported_content(context): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
context['course_id'] = CourseKey.from_string(context['course_id'])
|
||||
context['course_name'] = modulestore().get_course(context['course_id']).display_name
|
||||
|
||||
moderators = get_users_with_moderator_roles(context)
|
||||
context['site'] = Site.objects.get(id=context['site_id']
|
||||
)
|
||||
if not _is_content_still_reported(context):
|
||||
log.info('Reported content is no longer in reported state. Email to moderators will not be sent.')
|
||||
return
|
||||
for moderator in moderators:
|
||||
with emulate_http_request(site=context['site'], user=User.objects.get(id=context['user_id'])):
|
||||
message_context = _build_message_context_for_reported_content(context)
|
||||
message = ReportedContentNotification().personalize(
|
||||
Recipient(moderator.id, moderator.email),
|
||||
_get_course_language(context['course_id']),
|
||||
message_context
|
||||
)
|
||||
log.info(f'Sending forum reported content email notification with context {message_context}')
|
||||
ace.send(message)
|
||||
# TODO: add tracking for reported content email
|
||||
|
||||
|
||||
def _track_notification_sent(message, context):
|
||||
"""
|
||||
Send analytics event for a sent email
|
||||
@@ -121,6 +154,12 @@ def _should_send_message(context):
|
||||
)
|
||||
|
||||
|
||||
def _is_content_still_reported(context):
|
||||
if context.get('thread_id'):
|
||||
return len(cc.Thread.find(context['thread_id']).abuse_flaggers) > 0
|
||||
return len(cc.Comment.find(context['comment_id']).abuse_flaggers) > 0
|
||||
|
||||
|
||||
def _is_not_subcomment(comment_id):
|
||||
comment = cc.Comment.find(id=comment_id).retrieve()
|
||||
return not getattr(comment, 'parent_id', None)
|
||||
@@ -176,6 +215,15 @@ def _build_message_context(context): # lint-amnesty, pylint: disable=missing-fu
|
||||
return message_context
|
||||
|
||||
|
||||
def _build_message_context_for_reported_content(context): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
message_context = get_base_template_context(context['site'])
|
||||
message_context.update(context)
|
||||
message_context.update({
|
||||
'post_link': _get_thread_url(context),
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_body.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load django_markup %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
{% filter force_escape %}
|
||||
{% blocktrans trimmed asvar replied_to_text %}
|
||||
{{ course_name }} {{ course_id }} Reported content awaits review
|
||||
{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
{% interpolate_html replied_to_text start_tag='<b>'|safe end_tag='</b>'|safe %}
|
||||
</p>
|
||||
<div style="border-left: 1px solid rgba(0,0,0,0.25);
|
||||
padding: 1px 1px 1px 15px;
|
||||
margin: 20px 20px 30px 30px;
|
||||
color: rgba(0,0,0,.75);">
|
||||
{{ comment_body }}
|
||||
</div>
|
||||
|
||||
{% filter force_escape %}
|
||||
{% blocktrans asvar course_cta_text %}Go to Discussion{% endblocktrans %}
|
||||
{% endfilter %}
|
||||
{% include "ace_common/edx_ace/common/return_to_course_cta.html" with course_cta_text=course_cta_text course_cta_url=post_link%}
|
||||
|
||||
{% block google_analytics_pixel %}
|
||||
<img src="{{ ga_tracking_pixel_url }}" alt="" role="presentation" aria-hidden="true" style="display: block;"/>
|
||||
{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% blocktrans trimmed %}
|
||||
{{ course_name }} {{ course_id }} Reported content awaits review
|
||||
{% endblocktrans %}
|
||||
|
||||
|
||||
<div style="border-left: 1px solid rgba(0,0,0,0.25);
|
||||
padding: 1px 1px 1px 15px;
|
||||
margin: 20px 20px 30px 30px;
|
||||
color: rgba(0,0,0,.75);">
|
||||
<b> You are receiving this email because the following {{ content_type }} was reported for review </b>
|
||||
{{ comment_body }}
|
||||
</div>
|
||||
|
||||
<a href="{{ post_link }}"> {% trans "Go to Discussion" %} </a>
|
||||
{% endblock %}
|
||||
|
||||
{% block google_analytics_pixel %}
|
||||
<img src="{{ ga_tracking_pixel_url }}" alt="" role="presentation" aria-hidden="true" style="display: block;"/>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ platform_name }}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends 'ace_common/edx_ace/common/base_head.html' %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% blocktrans %} {{ course_name }} {{ course_id }} moderator content for review {% endblocktrans %}
|
||||
14
lms/djangoapps/discussion/toggles_utils.py
Normal file
14
lms/djangoapps/discussion/toggles_utils.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Utils for Discussions feature toggles
|
||||
"""
|
||||
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_EMAIL_NOTIFICATIONS
|
||||
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings
|
||||
|
||||
|
||||
def reported_content_email_notification_enabled(course_key):
|
||||
"""
|
||||
Checks for relevant flag and setting and returns boolean for reported
|
||||
content email notification for course
|
||||
"""
|
||||
return bool(ENABLE_REPORTED_CONTENT_EMAIL_NOTIFICATIONS.is_enabled(course_key) and
|
||||
CourseDiscussionSettings.get(course_key).reported_content_email_notifications)
|
||||
@@ -11,8 +11,10 @@ thread_voted = Signal()
|
||||
thread_deleted = Signal()
|
||||
thread_followed = Signal()
|
||||
thread_unfollowed = Signal()
|
||||
thread_flagged = Signal()
|
||||
comment_created = Signal()
|
||||
comment_edited = Signal()
|
||||
comment_voted = Signal()
|
||||
comment_deleted = Signal()
|
||||
comment_endorsed = Signal()
|
||||
comment_flagged = Signal()
|
||||
|
||||
Reference in New Issue
Block a user