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

Added notification for reported content #34067

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 10 additions & 2 deletions lms/djangoapps/discussion/django_comment_client/base/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,8 @@ def test_update_thread(self, mock_request):


@ddt.ddt
@disable_signal(views, 'comment_flagged')
@disable_signal(views, 'thread_flagged')
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
class ViewsTestCase(
ForumsEnableMixin,
Expand Down Expand Up @@ -1714,7 +1716,13 @@ def test_comment_actions(self, user, commentable_id, status_code, mock_request):
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
{"closed": False, "commentable_id": commentable_id, "thread_id": "dummy_thread", "body": 'dummy body'},
{
"closed": False,
"commentable_id": commentable_id,
"thread_id": "dummy_thread",
"body": 'dummy body',
"course_id": str(self.course.id)
},
)
for action in ["upvote_comment", "downvote_comment", "un_flag_abuse_for_comment", "flag_abuse_for_comment"]:
response = self.client.post(
Expand All @@ -1735,7 +1743,7 @@ def test_threads_actions(self, user, commentable_id, status_code, mock_request):
commentable_id = getattr(self, commentable_id)
self._setup_mock(
user, mock_request,
{"closed": False, "commentable_id": commentable_id, "body": "dummy body"},
{"closed": False, "commentable_id": commentable_id, "body": "dummy body", "course_id": str(self.course.id)}
)
for action in ["upvote_thread", "downvote_thread", "un_flag_abuse_for_thread", "flag_abuse_for_thread",
"follow_thread", "unfollow_thread"]:
Expand Down
59 changes: 52 additions & 7 deletions lms/djangoapps/discussion/rest_api/discussions_notifications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Discussion notifications sender util.
"""
import re

from django.conf import settings
from lms.djangoapps.discussion.django_comment_client.permissions import get_team
from openedx_events.learning.data import UserNotificationData, CourseNotificationData
Expand Down Expand Up @@ -70,7 +72,7 @@ def _send_course_wide_notification(self, notification_type, audience_filters=Non
course_key=self.course.id,
content_context={
"replier_name": self.creator.username,
"post_title": self.thread.title,
"post_title": getattr(self.thread, 'title', ''),
"course_name": self.course.display_name,
"sender_id": self.creator.id,
**extra_context,
Expand Down Expand Up @@ -201,16 +203,20 @@ def _create_cohort_course_audience(self):
discussion_cohorted = is_discussion_cohorted(course_key_str)

# Retrieves cohort divided discussion
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
try:
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
except CourseDiscussionSettings.DoesNotExist:
return {}
divided_course_wide_discussions, divided_inline_discussions = get_divided_discussions(
self.course,
discussion_settings
)

# Checks if post has any cohort assigned
group_id = self.thread.attributes['group_id']
if group_id is not None:
group_id = int(group_id)
group_id = self.thread.attributes.get('group_id')
if group_id is None:
return {}
group_id = int(group_id)

# Course wide topics
all_topics = divided_inline_discussions + divided_course_wide_discussions
Expand Down Expand Up @@ -259,11 +265,50 @@ def send_new_thread_created_notification(self):
}
self._send_course_wide_notification(notification_type, audience_filters, context)

def send_reported_content_notification(self):
"""
Send notification to users who are subscribed to the thread.
"""
thread_body = self.thread.body if self.thread.body else ''

thread_body = remove_html_tags(thread_body)
thread_types = {
# numeric key is the depth of the thread in the discussion
'comment': {
1: 'comment',
0: 'response'
},
'thread': {
0: 'thread'
}
}

content_type = thread_types[self.thread.type][getattr(self.thread, 'depth', 0)]

context = {
'username': self.creator.username,
'content_type': content_type,
'content': thread_body
}
audience_filters = self._create_cohort_course_audience()
audience_filters['discussion_roles'] = [
FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
]
self._send_course_wide_notification("content_reported", audience_filters, context)


def is_discussion_cohorted(course_key_str):
"""
Returns if the discussion is divided by cohorts
"""
cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key_str)
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
try:
cohort_settings = CourseCohortsSettings.objects.get(course_id=course_key_str)
discussion_settings = CourseDiscussionSettings.objects.get(course_id=course_key_str)
except (CourseCohortsSettings.DoesNotExist, CourseDiscussionSettings.DoesNotExist):
return False
return cohort_settings.is_cohorted and discussion_settings.always_divide_inline_discussions


def remove_html_tags(text):
clean = re.compile('<.*?>')
return re.sub(clean, '', text)
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Unit tests for the DiscussionNotificationSender class
"""

import unittest
from unittest.mock import MagicMock, patch

import pytest
from edx_toggles.toggles.testutils import override_waffle_flag

from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS


@patch('lms.djangoapps.discussion.rest_api.discussions_notifications.DiscussionNotificationSender'
'._create_cohort_course_audience', return_value={})
@patch('lms.djangoapps.discussion.rest_api.discussions_notifications.DiscussionNotificationSender'
'._send_course_wide_notification')
@pytest.mark.django_db
class TestDiscussionNotificationSender(unittest.TestCase):
"""
Tests for the DiscussionNotificationSender class
"""

@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, True)
def setUp(self):
self.thread = MagicMock()
self.course = MagicMock()
self.creator = MagicMock()
self.notification_sender = DiscussionNotificationSender(self.thread, self.course, self.creator)

def _setup_thread(self, thread_type, body, depth):
"""
Helper to set up the thread object
"""
self.thread.type = thread_type
self.thread.body = body
self.thread.depth = depth
self.creator.username = 'test_user'

def _assert_send_notification_called_with(self, mock_send_notification, expected_content_type):
"""
Helper to assert that the send_notification method was called with the correct arguments
"""
notification_type, audience_filters, context = mock_send_notification.call_args[0]
mock_send_notification.assert_called_once()

self.assertEqual(notification_type, "content_reported")
self.assertEqual(context, {
'username': 'test_user',
'content_type': expected_content_type,
'content': 'Thread body'
})
self.assertEqual(audience_filters, {
'discussion_roles': ['Administrator', 'Moderator', 'Community TA']
})

def test_send_reported_content_notification_for_response(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
arguments for a comment with depth 0
"""
self._setup_thread('comment', '<p>Thread body</p>', 0)
mock_create_audience.return_value = {}

self.notification_sender.send_reported_content_notification()

self._assert_send_notification_called_with(mock_send_notification, 'response')

def test_send_reported_content_notification_for_comment(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
arguments for a comment with depth 1
"""
self._setup_thread('comment', '<p>Thread body</p>', 1)
mock_create_audience.return_value = {}

self.notification_sender.send_reported_content_notification()

self._assert_send_notification_called_with(mock_send_notification, 'comment')

def test_send_reported_content_notification_for_thread(self, mock_send_notification, mock_create_audience):
"""
Test that the send_reported_content_notification method calls the send_notification method with the correct
"""
self._setup_thread('thread', '<p>Thread body</p>', 0)
mock_create_audience.return_value = {}

self.notification_sender.send_reported_content_notification()

self._assert_send_notification_called_with(mock_send_notification, 'thread')
33 changes: 27 additions & 6 deletions lms/djangoapps/discussion/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
Signal handlers related to discussions.
"""


import logging

from django.conf import settings
from django.dispatch import receiver
from django.utils.html import strip_tags
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from xmodule.modulestore.django import SignalHandler

from lms.djangoapps.discussion.rest_api.discussions_notifications import DiscussionNotificationSender
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from xmodule.modulestore.django import SignalHandler, modulestore

from lms.djangoapps.discussion import tasks
from lms.djangoapps.discussion.rest_api.tasks import send_response_notifications, send_thread_created_notification
Expand All @@ -19,7 +22,6 @@

log = logging.getLogger(__name__)


ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY = 'enable_forum_notifications'


Expand All @@ -43,7 +45,8 @@ def update_discussions_on_course_publish(sender, course_key, **kwargs): # pylin


@receiver(signals.comment_created)
def send_discussion_email_notification(sender, user, post, **kwargs): # lint-amnesty, pylint: disable=missing-function-docstring, unused-argument
def send_discussion_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)
Expand All @@ -64,7 +67,10 @@ def send_discussion_email_notification(sender, user, post, **kwargs): # lint-am

@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
def send_reported_content_email_notification(sender, user, post, **kwargs):
"""
Sends email notification for reported content.
"""
current_site = get_current_site()
if current_site is None:
log.info('Discussion: No current site, not sending notification about post: %s.', post.id)
Expand All @@ -84,6 +90,19 @@ def send_reported_content_email_notification(sender, user, post, **kwargs): # l
send_message_for_reported_content(user, post, current_site, sender)


@receiver(signals.comment_flagged)
@receiver(signals.thread_flagged)
def send_reported_content_notification(sender, user, post, **kwargs):
"""
Sends notification for reported content.
"""
course_key = CourseKey.from_string(post.course_id)
if not ENABLE_REPORTED_CONTENT_NOTIFICATIONS.is_enabled(course_key):
return
course = modulestore().get_course(course_key)
DiscussionNotificationSender(post, course, user).send_reported_content_notification()


def create_message_context(comment, site):
thread = comment.thread
return {
Expand All @@ -105,6 +124,7 @@ def create_message_context_for_reported_content(user, post, site, sender):
"""
Create message context for reported content.
"""

def get_comment_type(comment):
"""
Returns type of comment.
Expand All @@ -131,7 +151,8 @@ def send_message(comment, site): # lint-amnesty, pylint: disable=missing-functi
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
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)

Expand Down
12 changes: 12 additions & 0 deletions lms/djangoapps/discussion/toggles.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@
# .. toggle_creation_date: 2021-11-05
# .. toggle_target_removal_date: 2022-12-05
ENABLE_DISCUSSIONS_MFE = CourseWaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.enable_discussions_mfe', __name__)

# .. toggle_name: discussions.enable_reported_content_notifications
# .. toggle_implementation: CourseWaffleFlag
# .. toggle_default: False
# .. toggle_description: Waffle flag to enable reported content notifications.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 18-Jan-2024
# .. toggle_target_removal_date: 18-Feb-2024
ENABLE_REPORTED_CONTENT_NOTIFICATIONS = CourseWaffleFlag(
f'{WAFFLE_FLAG_NAMESPACE}.enable_reported_content_notifications',
__name__
)
19 changes: 19 additions & 0 deletions openedx/core/djangoapps/notifications/base_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@
'email_template': '',
'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE]
},
'content_reported': {
'notification_app': 'discussion',
'name': 'content_reported',
'is_core': False,
'info': '',
'web': True,
'email': True,
'push': True,
'non_editable': [],
'content_template': _('<p><strong>{username}’s </strong> {content_type} has been reported <strong> {'
'content}</strong></p>'),

'content_context': {
'post_title': 'Post title',
'author_name': 'author name',
'replier_name': 'replier name',
},
'email_template': '',
},
}

COURSE_NOTIFICATION_APPS = {
Expand Down
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
NOTIFICATION_CHANNELS = ['web', 'push', 'email']

# Update this version when there is a change to any course specific notification type or app.
COURSE_NOTIFICATION_CONFIG_VERSION = 4
COURSE_NOTIFICATION_CONFIG_VERSION = 5


def get_course_notification_preference_config():
Expand Down
3 changes: 3 additions & 0 deletions openedx/core/djangoapps/notifications/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
Expand Down Expand Up @@ -169,6 +170,7 @@ def test_course_enrollment_post_save(self):


@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
@ddt.ddt
class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
"""
Expand Down Expand Up @@ -246,6 +248,7 @@ def _expected_api_response(self, course=None):
},
'new_discussion_post': {'web': False, 'email': False, 'push': False, 'info': ''},
'new_question_post': {'web': False, 'email': False, 'push': False, 'info': ''},
'content_reported': {'web': True, 'email': True, 'push': True, 'info': ''},
},
'non_editable': {
'core': ['web']
Expand Down
Loading
Loading