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:
Zachary Hancock
2024-02-13 13:16:23 -05:00
committed by GitHub
parent 57b480b04f
commit 2f2ed4d6cb
6 changed files with 216 additions and 12 deletions

View File

@@ -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,

View File

@@ -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.

View File

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

View File

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

View File

@@ -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.

View File

@@ -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/"