Skip to content

Commit

Permalink
feat: added reported notification type
Browse files Browse the repository at this point in the history
feat: added new notification for reported content

feat: added reported notifications

feat: added reported notifications

fix: resolved linter errors

fix: resolved linter errors

feat: added flag for reported content

fix: removed redundant check

fix: removed redundant check

fix: added flag in notification pref serializer

fix: added reported content flag to unit test
  • Loading branch information
AhtishamShahid committed Jan 30, 2024
1 parent ffd4a54 commit 8f837bf
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 16 deletions.
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

0 comments on commit 8f837bf

Please sign in to comment.