feat: added reported notification type (#34067)
This commit is contained in:
@@ -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"]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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__
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user