feat: added reported notification type (#34067)

This commit is contained in:
Ahtisham Shahid
2024-01-30 12:07:49 +05:00
committed by GitHub
parent d4536ed79d
commit 039583f88f
9 changed files with 220 additions and 16 deletions

View File

@@ -426,6 +426,8 @@ class ViewsQueryCountTestCase(
@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,
@@ -1714,7 +1716,13 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
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(
@@ -1735,7 +1743,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
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"]:

View File

@@ -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
@@ -70,7 +72,7 @@ class DiscussionNotificationSender:
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,
@@ -201,16 +203,20 @@ class DiscussionNotificationSender:
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
@@ -259,11 +265,50 @@ class DiscussionNotificationSender:
}
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)

View File

@@ -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')

View File

@@ -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
@@ -19,7 +22,6 @@ from openedx.core.djangoapps.theming.helpers import get_current_site
log = logging.getLogger(__name__)
ENABLE_FORUM_NOTIFICATIONS_FOR_SITE_KEY = 'enable_forum_notifications'
@@ -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)
@@ -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)
@@ -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 {
@@ -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.
@@ -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)

View File

@@ -12,3 +12,15 @@ from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
# .. 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__
)

View File

@@ -113,6 +113,25 @@ COURSE_NOTIFICATION_TYPES = {
'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 = {

View File

@@ -21,7 +21,7 @@ log = logging.getLogger(__name__)
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():

View File

@@ -18,6 +18,7 @@ from rest_framework.test import APIClient, APITestCase
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,
@@ -169,6 +170,7 @@ class CourseEnrollmentPostSaveTest(ModuleStoreTestCase):
@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True)
@override_waffle_flag(ENABLE_REPORTED_CONTENT_NOTIFICATIONS, active=True)
@ddt.ddt
class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
"""
@@ -246,6 +248,7 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase):
},
'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']

View File

@@ -4,6 +4,7 @@ Utils function for notifications app
from typing import Dict, List
from common.djangoapps.student.models import CourseEnrollment
from lms.djangoapps.discussion.toggles import ENABLE_REPORTED_CONTENT_NOTIFICATIONS
from openedx.core.djangoapps.django_comment_common.models import Role
from openedx.core.lib.cache_utils import request_cached
@@ -65,6 +66,10 @@ def filter_course_wide_preferences(course_key, preferences):
if ENABLE_COURSEWIDE_NOTIFICATIONS.is_enabled(course_key):
return preferences
course_wide_notification_types = ['new_discussion_post', 'new_question_post']
if not ENABLE_REPORTED_CONTENT_NOTIFICATIONS.is_enabled(course_key):
course_wide_notification_types.append('content_reported')
config = preferences['notification_preference_config']
for app_prefs in config.values():
notification_types = app_prefs['notification_types']