feat: move new forum content creation notifications to use course wide notification event

This commit is contained in:
SaadYousaf
2023-12-11 14:43:32 +05:00
committed by Saad Yousaf
parent 17a58cc9a0
commit 2d353f5594
5 changed files with 304 additions and 100 deletions

View File

@@ -4,9 +4,14 @@ Audience based filters for notifications
from abc import abstractmethod
from opaque_keys.edx.keys import CourseKey
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseStaffRole, CourseInstructorRole
from lms.djangoapps.discussion.django_comment_client.utils import get_users_with_roles
from lms.djangoapps.teams.models import CourseTeam
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
@@ -33,11 +38,10 @@ class NotificationAudienceFilterBase:
pass
class RoleAudienceFilter(NotificationAudienceFilterBase):
class ForumRoleAudienceFilter(NotificationAudienceFilterBase):
"""
Filter class for roles
"""
# TODO: Add course roles to this. We currently support only forum roles
allowed_filters = [
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_MODERATOR,
@@ -55,6 +59,36 @@ class RoleAudienceFilter(NotificationAudienceFilterBase):
return [user.id for user in get_users_with_roles(roles, self.course_key)]
class CourseRoleAudienceFilter(NotificationAudienceFilterBase):
"""
Filter class for course roles
"""
allowed_filters = ['staff', 'instructor']
def filter(self, course_roles):
"""
Filter users based on their course roles
"""
if not self.is_valid_filter(course_roles):
raise ValueError(f'Invalid roles {course_roles} passed to CourseRoleAudienceFilter')
user_ids = []
course_key = self.course_key
if not isinstance(course_key, CourseKey):
course_key = CourseKey.from_string(course_key)
if 'staff' in course_roles:
staff_users = CourseStaffRole(course_key).users_with_role().values_list('id', flat=True)
user_ids.extend(staff_users)
if 'instructor' in course_roles:
instructor_users = CourseInstructorRole(course_key).users_with_role().values_list('id', flat=True)
user_ids.extend(instructor_users)
return user_ids
class EnrollmentAudienceFilter(NotificationAudienceFilterBase):
"""
Filter class for enrollment modes
@@ -71,3 +105,39 @@ class EnrollmentAudienceFilter(NotificationAudienceFilterBase):
course_id=self.course_key,
mode__in=enrollment_modes,
).values_list('user_id', flat=True)
class TeamAudienceFilter(NotificationAudienceFilterBase):
"""
Filter class for team roles
"""
def filter(self, team_ids):
"""
Filter users based on team id
"""
teams = CourseTeam.objects.filter(team_id__in=team_ids, course_id=self.course_key)
if not teams: # invalid team ids passed
raise ValueError(f'Invalid Team ids {team_ids} passed to TeamAudienceFilter for course {self.course_key}')
user_ids = []
for team in teams:
user_ids.extend(team.users.all().values_list('id', flat=True))
return user_ids
class CohortAudienceFilter(NotificationAudienceFilterBase):
"""
Filter class for cohort roles
"""
def filter(self, group_ids):
"""
Filter users based on their cohort ids
"""
users_in_cohort = CourseUserGroup.objects.filter(
course_id=self.course_key, id__in=group_ids
).values_list('users__id', flat=True)
return users_in_cohort

View File

@@ -14,17 +14,24 @@ from openedx_events.learning.signals import (
)
from common.djangoapps.student.models import CourseEnrollment
from openedx.core.djangoapps.notifications.audience_filters import RoleAudienceFilter, EnrollmentAudienceFilter
from openedx.core.djangoapps.notifications.audience_filters import (
ForumRoleAudienceFilter,
EnrollmentAudienceFilter,
TeamAudienceFilter,
CohortAudienceFilter,
CourseRoleAudienceFilter,
)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference
log = logging.getLogger(__name__)
AUDIENCE_FILTER_TYPES = ['role', 'enrollment']
AUDIENCE_FILTER_CLASSES = {
'role': RoleAudienceFilter,
'enrollment': EnrollmentAudienceFilter,
'discussion_roles': ForumRoleAudienceFilter,
'course_roles': CourseRoleAudienceFilter,
'enrollments': EnrollmentAudienceFilter,
'teams': TeamAudienceFilter,
'cohorts': CohortAudienceFilter,
}
@@ -76,11 +83,15 @@ def calculate_course_wide_notification_audience(course_key, audience_filters):
Calculate the audience for a course-wide notification based on the audience filters
"""
if not audience_filters:
return CourseEnrollment.objects.filter(course_id=course_key, is_active=True).values_list('user_id', flat=True)
active_enrollments = CourseEnrollment.objects.filter(
course_id=course_key,
is_active=True
).values_list('user_id', flat=True)
return list(active_enrollments)
audience_user_ids = []
for filter_type, filter_values in audience_filters.items():
if filter_type in AUDIENCE_FILTER_TYPES:
if filter_type in AUDIENCE_FILTER_CLASSES.keys(): # lint-amnesty, pylint: disable=consider-iterating-dictionary
filter_class = AUDIENCE_FILTER_CLASSES.get(filter_type)
if filter_class:
filter_instance = filter_class(course_key)
@@ -93,20 +104,24 @@ def calculate_course_wide_notification_audience(course_key, audience_filters):
@receiver(COURSE_NOTIFICATION_REQUESTED)
def generate_course_notifications(signal, sender, notification_data, metadata, **kwargs):
def generate_course_notifications(signal, sender, course_notification_data, metadata, **kwargs):
"""
Watches for COURSE_NOTIFICATION_REQUESTED signal and calls send_notifications task
"""
from openedx.core.djangoapps.notifications.tasks import send_notifications
notification_data = notification_data.__dict__
notification_data['course_key'] = str(notification_data['course_key'])
audience_filters = notification_data.pop('audience_filters')
user_ids = calculate_course_wide_notification_audience(
notification_data['course_key'],
audience_filters,
)
notification_data['user_ids'] = user_ids
notification_data['context'] = notification_data.pop('content_context')
from openedx.core.djangoapps.notifications.tasks import send_notifications
course_notification_data = course_notification_data.__dict__
notification_data = {
'course_key': str(course_notification_data['course_key']),
'user_ids': calculate_course_wide_notification_audience(
str(course_notification_data['course_key']),
course_notification_data['audience_filters'],
),
'context': course_notification_data.get('content_context'),
'app_name': course_notification_data.get('app_name'),
'notification_type': course_notification_data.get('notification_type'),
'content_url': course_notification_data.get('content_url'),
}
send_notifications.delay(**notification_data)

View File

@@ -9,9 +9,11 @@ from django.utils.timezone import now
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
from common.djangoapps.student.tests.factories import UserFactory, CourseEnrollmentFactory
from lms.djangoapps.teams.tests.factories import CourseTeamFactory, CourseTeamMembershipFactory
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
from openedx.core.djangoapps.django_comment_common.models import (
FORUM_ROLE_ADMINISTRATOR,
FORUM_ROLE_COMMUNITY_TA,
@@ -22,7 +24,10 @@ from openedx.core.djangoapps.django_comment_common.models import (
)
from openedx.core.djangoapps.notifications.audience_filters import (
EnrollmentAudienceFilter,
RoleAudienceFilter,
ForumRoleAudienceFilter,
CourseRoleAudienceFilter,
CohortAudienceFilter,
TeamAudienceFilter,
)
from openedx.core.djangoapps.notifications.filters import NotificationFilter
from openedx.core.djangoapps.notifications.handlers import calculate_course_wide_notification_audience
@@ -224,9 +229,9 @@ class TestEnrollmentAudienceFilter(ModuleStoreTestCase):
@ddt.ddt
class TestRoleAudienceFilter(ModuleStoreTestCase):
class TestForumRoleAudienceFilter(ModuleStoreTestCase):
"""
Tests for the RoleAudienceFilter.
Tests for the ForumRoleAudienceFilter.
"""
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
@@ -263,17 +268,134 @@ class TestRoleAudienceFilter(ModuleStoreTestCase):
)
@ddt.unpack
def test_valid_role_filter(self, role_names, expected_count):
role_filter = RoleAudienceFilter(self.course.id)
role_filter = ForumRoleAudienceFilter(self.course.id)
filtered_users = role_filter.filter(role_names)
self.assertEqual(len(filtered_users), expected_count)
def test_invalid_role_filter(self):
role_filter = RoleAudienceFilter(self.course.id)
role_filter = ForumRoleAudienceFilter(self.course.id)
role_names = ["INVALID_MODE"]
with self.assertRaises(ValueError):
role_filter.filter(role_names)
# TODO: Cleanup this test class
@ddt.ddt
class TestCourseRoleAudienceFilter(ModuleStoreTestCase):
"""
Tests for the CourseRoleAudienceFilter.
"""
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
self.course = CourseFactory()
self.students = [UserFactory() for _ in range(10)]
# Assign 5 users with course staff role
for student in self.students[:5]:
CourseStaffRole(self.course.id).add_users(student)
# Assign 5 users with course instructor role
for student in self.students[5:10]:
CourseInstructorRole(self.course.id).add_users(student)
@ddt.data(
(["instructor"], 5),
(["staff"], 5),
(["instructor", "staff"], 10),
)
@ddt.unpack
def test_valid_role_filter(self, role_names, expected_count):
course_role_filter = CourseRoleAudienceFilter(self.course.id)
filtered_users = course_role_filter.filter(role_names)
self.assertEqual(len(filtered_users), expected_count)
def test_invalid_role_filter(self):
course_role_filter = CourseRoleAudienceFilter(self.course.id)
role_names = ["INVALID_MODE"]
with self.assertRaises(ValueError):
course_role_filter.filter(role_names)
@ddt.ddt
class TestCohortAudienceFilter(ModuleStoreTestCase):
"""
Tests for the CohortAudienceFilter.
"""
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
self.course = CourseFactory()
self.cohort1_users = [UserFactory() for _ in range(3)]
self.cohort2_users = [UserFactory() for _ in range(2)]
users = self.cohort1_users + self.cohort2_users
for user in users:
CourseEnrollment.enroll(user, self.course.id)
self.cohort1 = CohortFactory(course_id=self.course.id, users=self.cohort1_users)
self.cohort2 = CohortFactory(course_id=self.course.id, users=self.cohort2_users)
@ddt.data(
([1], 3),
([2], 2),
([1, 2], 5),
)
@ddt.unpack
def test_valid_cohort_filter(self, cohort_ids, expected_count):
cohort_filter = CohortAudienceFilter(self.course.id)
filtered_users = cohort_filter.filter(cohort_ids)
self.assertEqual(len(filtered_users), expected_count)
def test_invalid_cohort_filter(self):
cohort_filter = CohortAudienceFilter(self.course.id)
cohort_ids = ["INVALID_MODE"]
with self.assertRaises(ValueError):
cohort_filter.filter(cohort_ids)
@ddt.ddt
class TestTeamAudienceFilter(ModuleStoreTestCase):
"""
Tests for the TeamAudienceFilter.
"""
def setUp(self):
super().setUp() # lint-amnesty, pylint: disable=super-with-arguments
self.course = CourseFactory()
self.teams = [CourseTeamFactory(course_id=self.course.id, team_id=f"team-{i}") for i in range(2)]
self.team1_users = [UserFactory() for _ in range(3)]
self.team2_users = [UserFactory() for _ in range(2)]
users = self.team1_users + self.team2_users
for user in users:
CourseEnrollment.enroll(user, self.course.id)
for user in self.team1_users:
CourseTeamMembershipFactory.create(team=self.teams[0], user=user)
for user in self.team2_users:
CourseTeamMembershipFactory.create(team=self.teams[1], user=user)
@ddt.data(
(["team-0"], 3),
(["team-1"], 2),
(["team-0", "team-1"], 5),
)
@ddt.unpack
def test_valid_team_filter(self, team_ids, expected_count):
team_filter = TeamAudienceFilter(self.course.id)
filtered_users = team_filter.filter(team_ids)
self.assertEqual(len(filtered_users), expected_count)
def test_invalid_team_filter(self):
team_filter = TeamAudienceFilter(self.course.id)
team_ids = ["INVALID_MODE"]
with self.assertRaises(ValueError):
team_filter.filter(team_ids)
@ddt.ddt
class TestAudienceFilter(ModuleStoreTestCase):
"""
@@ -310,16 +432,16 @@ class TestAudienceFilter(ModuleStoreTestCase):
@ddt.data(
({
"enrollment": ["verified"],
"role": ["Moderator"],
"enrollments": ["verified"],
"discussion_roles": ["Moderator"],
}, 15),
({
"enrollment": ["audit", "verified"],
"role": ["Administrator", "Student"],
"enrollments": ["audit", "verified"],
"discussion_roles": ["Administrator", "Student"],
}, 30),
({
"enrollment": ["audit", "honor", "verified"],
"role": ["Administrator", "Moderator", "Student", "Community TA"],
"enrollments": ["audit", "honor", "verified"],
"discussion_roles": ["Administrator", "Moderator", "Student", "Community TA"],
}, 30),
)
@ddt.unpack