feat: send course role events to the event bus (#34158)
Notify the event bus when a user's role in a course is added or removed
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
|
||||
from common.djangoapps.student.signals.signals import (
|
||||
emit_course_access_role_added,
|
||||
emit_course_access_role_removed,
|
||||
ENROLL_STATUS_CHANGE,
|
||||
ENROLLMENT_TRACK_UPDATED,
|
||||
REFUND_ORDER,
|
||||
|
||||
@@ -8,13 +8,14 @@ from asyncio.log import logger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
from django.db.models.signals import post_delete, post_save, pre_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from lms.djangoapps.courseware.toggles import courseware_mfe_progress_milestones_are_active
|
||||
from lms.djangoapps.utils import get_braze_client
|
||||
from common.djangoapps.student.helpers import EMAIL_EXISTS_MSG_FMT, USERNAME_EXISTS_MSG_FMT, AccountValidationError
|
||||
from common.djangoapps.student.models import (
|
||||
CourseAccessRole,
|
||||
CourseEnrollment,
|
||||
CourseEnrollmentCelebration,
|
||||
PendingNameChange,
|
||||
@@ -22,7 +23,11 @@ from common.djangoapps.student.models import (
|
||||
is_username_retired
|
||||
)
|
||||
from common.djangoapps.student.models_api import confirm_name_change
|
||||
from common.djangoapps.student.signals import USER_EMAIL_CHANGED
|
||||
from common.djangoapps.student.signals import (
|
||||
emit_course_access_role_added,
|
||||
emit_course_access_role_removed,
|
||||
USER_EMAIL_CHANGED,
|
||||
)
|
||||
from openedx.core.djangoapps.safe_sessions.middleware import EmailChangeMiddleware
|
||||
from openedx.features.name_affirmation_api.utils import is_name_affirmation_installed
|
||||
|
||||
@@ -87,6 +92,29 @@ def create_course_enrollment_celebration(sender, instance, created, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseAccessRole)
|
||||
def on_course_access_role_created(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Emit an event to the event-bus when a CourseAccessRole is created
|
||||
"""
|
||||
# Updating a role instance to a different role is unhandled behavior at the moment
|
||||
# this event assumes roles are only created or deleted
|
||||
if not created:
|
||||
return
|
||||
|
||||
user = instance.user
|
||||
emit_course_access_role_added(user, instance.course_id, instance.org, instance.role)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CourseAccessRole)
|
||||
def listen_for_course_access_role_removed(sender, instance, **kwargs):
|
||||
"""
|
||||
Emit an event to the event-bus when a CourseAccessRole is deleted
|
||||
"""
|
||||
user = instance.user
|
||||
emit_course_access_role_removed(user, instance.course_id, instance.org, instance.role)
|
||||
|
||||
|
||||
def listen_for_verified_name_approved(sender, user_id, profile_name, **kwargs):
|
||||
"""
|
||||
If the user has a pending name change that corresponds to an approved verified name, confirm it.
|
||||
|
||||
@@ -5,6 +5,10 @@ Enrollment track related signals.
|
||||
|
||||
from django.dispatch import Signal
|
||||
|
||||
from openedx_events.learning.data import CourseAccessRoleData, UserData, UserPersonalData
|
||||
from openedx_events.learning.signals import COURSE_ACCESS_ROLE_ADDED, COURSE_ACCESS_ROLE_REMOVED
|
||||
|
||||
|
||||
# The purely documentational providing_args argument for Signal is deprecated.
|
||||
# So we are moving the args to a comment.
|
||||
|
||||
@@ -21,3 +25,45 @@ ENROLL_STATUS_CHANGE = Signal()
|
||||
REFUND_ORDER = Signal()
|
||||
|
||||
USER_EMAIL_CHANGED = Signal()
|
||||
|
||||
|
||||
def emit_course_access_role_added(user, course_id, org_key, role):
|
||||
"""
|
||||
Emit an event to the event-bus when a CourseAccessRole is added
|
||||
"""
|
||||
COURSE_ACCESS_ROLE_ADDED.send_event(
|
||||
course_access_role_data=CourseAccessRoleData(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
course_key=course_id,
|
||||
org_key=org_key,
|
||||
role=role,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def emit_course_access_role_removed(user, course_id, org_key, role):
|
||||
"""
|
||||
Emit an event to the event-bus when a CourseAccessRole is deleted
|
||||
"""
|
||||
COURSE_ACCESS_ROLE_REMOVED.send_event(
|
||||
course_access_role_data=CourseAccessRoleData(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
),
|
||||
id=user.id,
|
||||
is_active=user.is_active,
|
||||
),
|
||||
course_key=course_id,
|
||||
org_key=org_key,
|
||||
role=role,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,32 +4,37 @@ Test that various events are fired for models in the student app.
|
||||
|
||||
|
||||
from unittest import mock
|
||||
import pytest
|
||||
|
||||
import ddt
|
||||
import pytest
|
||||
from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
from django_countries.fields import Country
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollmentAllowed, CourseEnrollment
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory, UserProfileFactory
|
||||
from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx_events.learning.data import ( # lint-amnesty, pylint: disable=wrong-import-order
|
||||
CourseAccessRoleData,
|
||||
CourseData,
|
||||
CourseEnrollmentData,
|
||||
UserData,
|
||||
UserPersonalData,
|
||||
UserPersonalData
|
||||
)
|
||||
from openedx_events.learning.signals import ( # lint-amnesty, pylint: disable=wrong-import-order
|
||||
COURSE_ACCESS_ROLE_ADDED,
|
||||
COURSE_ACCESS_ROLE_REMOVED,
|
||||
COURSE_ENROLLMENT_CHANGED,
|
||||
COURSE_ENROLLMENT_CREATED,
|
||||
COURSE_UNENROLLMENT_COMPLETED,
|
||||
COURSE_UNENROLLMENT_COMPLETED
|
||||
)
|
||||
from openedx_events.tests.utils import OpenEdxEventsTestMixin # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentAllowedFactory, UserFactory, UserProfileFactory
|
||||
from common.djangoapps.student.tests.tests import UserSettingsEventTestMixin
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import \
|
||||
SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@@ -377,3 +382,109 @@ class EnrollmentEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin):
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@ddt.ddt
|
||||
class TestCourseAccessRoleEvents(TestCase, OpenEdxEventsTestMixin):
|
||||
"""
|
||||
Tests for the events associated with the CourseAccessRole model.
|
||||
"""
|
||||
ENABLED_OPENEDX_EVENTS = [
|
||||
'org.openedx.learning.user.course_access_role.added.v1',
|
||||
'org.openedx.learning.user.course_access_role.removed.v1',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.start_events_isolation()
|
||||
|
||||
def setUp(self):
|
||||
self.course_key = CourseKey.from_string("course-v1:test+blah+blah")
|
||||
self.user = UserFactory.create(
|
||||
username="test",
|
||||
email="test@example.com",
|
||||
password="password",
|
||||
)
|
||||
self.receiver_called = False
|
||||
|
||||
def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Used show that the Open edX Event was called by the Django signal handler.
|
||||
"""
|
||||
self.receiver_called = True
|
||||
|
||||
@ddt.data(
|
||||
CourseStaffRole,
|
||||
CourseInstructorRole,
|
||||
)
|
||||
def test_access_role_created_event_emitted(self, AccessRole):
|
||||
"""
|
||||
Event is emitted with the correct data when a CourseAccessRole is created.
|
||||
"""
|
||||
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
|
||||
COURSE_ACCESS_ROLE_ADDED.connect(event_receiver)
|
||||
|
||||
role = AccessRole(self.course_key)
|
||||
role.add_users(self.user)
|
||||
|
||||
self.assertTrue(self.receiver_called)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": COURSE_ACCESS_ROLE_ADDED,
|
||||
"sender": None,
|
||||
"course_access_role_data": CourseAccessRoleData(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=self.user.username,
|
||||
email=self.user.email,
|
||||
),
|
||||
id=self.user.id,
|
||||
is_active=self.user.is_active,
|
||||
),
|
||||
course_key=self.course_key,
|
||||
org_key=self.course_key.org,
|
||||
role=role._role_name, # pylint: disable=protected-access
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
CourseStaffRole,
|
||||
CourseInstructorRole,
|
||||
)
|
||||
def test_access_role_removed_event_emitted(self, AccessRole):
|
||||
"""
|
||||
Event is emitted with the correct data when a CourseAccessRole is deleted.
|
||||
"""
|
||||
role = AccessRole(self.course_key)
|
||||
role.add_users(self.user)
|
||||
|
||||
# connect mock only after initial role is added
|
||||
event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect)
|
||||
COURSE_ACCESS_ROLE_REMOVED.connect(event_receiver)
|
||||
role.remove_users(self.user)
|
||||
|
||||
self.assertTrue(self.receiver_called)
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"signal": COURSE_ACCESS_ROLE_REMOVED,
|
||||
"sender": None,
|
||||
"course_access_role_data": CourseAccessRoleData(
|
||||
user=UserData(
|
||||
pii=UserPersonalData(
|
||||
username=self.user.username,
|
||||
email=self.user.email,
|
||||
),
|
||||
id=self.user.id,
|
||||
is_active=self.user.is_active,
|
||||
),
|
||||
course_key=self.course_key,
|
||||
org_key=self.course_key.org,
|
||||
role=role._role_name, # pylint: disable=protected-access
|
||||
),
|
||||
},
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@@ -5455,6 +5455,14 @@ EVENT_BUS_PRODUCER_CONFIG = {
|
||||
# .. toggle_tickets: https://github.com/openedx/openedx-events/issues/210
|
||||
'enabled': False}
|
||||
},
|
||||
'org.openedx.learning.user.course_access_role.added.v1': {
|
||||
'learning-course-access-role-lifecycle':
|
||||
{'event_key_field': 'course_access_role_data.course_key', 'enabled': False},
|
||||
},
|
||||
'org.openedx.learning.user.course_access_role.removed.v1': {
|
||||
'learning-course-access-role-lifecycle':
|
||||
{'event_key_field': 'course_access_role_data.course_key', 'enabled': False},
|
||||
},
|
||||
# CMS events. These have to be copied over here because cms.common adds some derived entries as well,
|
||||
# and the derivation fails if the keys are missing. If we ever fully decouple the lms and cms settings,
|
||||
# we can remove these.
|
||||
|
||||
@@ -521,6 +521,15 @@ certificate_revoked_event_config['learning-certificate-lifecycle']['enabled'] =
|
||||
certificate_created_event_config = EVENT_BUS_PRODUCER_CONFIG['org.openedx.learning.certificate.created.v1']
|
||||
certificate_created_event_config['learning-certificate-lifecycle']['enabled'] = True
|
||||
|
||||
course_access_role_added_event_setting = EVENT_BUS_PRODUCER_CONFIG[
|
||||
'org.openedx.learning.user.course_access_role.added.v1'
|
||||
]
|
||||
course_access_role_added_event_setting['learning-course-access-role-lifecycle']['enabled'] = True
|
||||
course_access_role_removed_event_setting = EVENT_BUS_PRODUCER_CONFIG[
|
||||
'org.openedx.learning.user.course_access_role.removed.v1'
|
||||
]
|
||||
course_access_role_removed_event_setting['learning-course-access-role-lifecycle']['enabled'] = True
|
||||
|
||||
######################## Subscriptions API SETTINGS ########################
|
||||
SUBSCRIPTIONS_ROOT_URL = "http://host.docker.internal:18750"
|
||||
SUBSCRIPTIONS_API_PATH = f"{SUBSCRIPTIONS_ROOT_URL}/api/v1/stripe-subscription/"
|
||||
|
||||
Reference in New Issue
Block a user