From 07e16b2123c3eeaf66c9753dec8140eed9467838 Mon Sep 17 00:00:00 2001 From: Kyrylo Kholodenko Date: Mon, 15 Apr 2024 22:05:24 +0300 Subject: [PATCH 1/7] feat: emit new course passing status events --- cms/envs/common.py | 34 +++++ lms/djangoapps/grades/events.py | 87 ++++++++++++ lms/djangoapps/grades/tests/test_events.py | 152 ++++++++++++++++++++- lms/envs/common.py | 40 ++++++ 4 files changed, 309 insertions(+), 4 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index e684946d74af..a777307089ec 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -583,6 +583,14 @@ # See annotations in lms/envs/common.py for details. 'ENABLE_BLAKE2B_HASHING': False, + + # .. toggle_name: FEATURES['BADGES_ENABLED'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to enable the Badges feature. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2024-04-10 + 'BADGES_ENABLED': False, } # .. toggle_name: ENABLE_COPPA_COMPLIANCE @@ -2880,6 +2888,8 @@ def _should_send_xblock_events(settings): return settings.FEATURES['ENABLE_SEND_XBLOCK_LIFECYCLE_EVENTS_OVER_BUS'] +def _should_send_learning_badge_events(settings): + return settings.FEATURES['BADGES_ENABLED'] # .. setting_name: EVENT_BUS_PRODUCER_CONFIG # .. setting_default: all events disabled @@ -2930,6 +2940,18 @@ def _should_send_xblock_events(settings): 'learning-certificate-lifecycle': {'event_key_field': 'certificate.course.course_key', 'enabled': False}, }, + "org.openedx.learning.course.passing.status.updated.v1": { + "learning-badges-lifecycle": { + "event_key_field": "course_passing_status.course.course_key", + "enabled": _should_send_learning_badge_events, + }, + }, + "org.openedx.learning.ccx.course.passing.status.updated.v1": { + "learning-badges-lifecycle": { + "event_key_field": "course_passing_status.course.course_key", + "enabled": _should_send_learning_badge_events, + }, + }, } @@ -2940,6 +2962,18 @@ def _should_send_xblock_events(settings): derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.content_authoring.xblock.deleted.v1', 'course-authoring-xblock-lifecycle', 'enabled') +derived_collection_entry( + "EVENT_BUS_PRODUCER_CONFIG", + "org.openedx.learning.course.passing.status.updated.v1", + "learning-badges-lifecycle", + "enabled", +) +derived_collection_entry( + "EVENT_BUS_PRODUCER_CONFIG", + "org.openedx.learning.ccx.course.passing.status.updated.v1", + "learning-badges-lifecycle", + "enabled", +) ################### Authoring API ###################### diff --git a/lms/djangoapps/grades/events.py b/lms/djangoapps/grades/events.py index 90279a3e69fe..538f3b34eef3 100644 --- a/lms/djangoapps/grades/events.py +++ b/lms/djangoapps/grades/events.py @@ -6,6 +6,15 @@ from crum import get_current_user from django.conf import settings from eventtracking import tracker +from openedx_events.learning.data import ( + CcxCourseData, + CcxCoursePassingStatusData, + CourseData, + CoursePassingStatusData, + UserData, + UserPersonalData +) +from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment @@ -190,6 +199,45 @@ def course_grade_now_passed(user, course_id): } ) + # produce to event bus + if hasattr(course_id, 'ccx'): + CCX_COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CcxCoursePassingStatusData( + status=CcxCoursePassingStatusData.PASSING, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CcxCourseData( + ccx_course_key=course_id, + master_course_key=course_id.to_course_locator(), + ), + ) + ) + else: + COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CoursePassingStatusData( + status=CoursePassingStatusData.PASSING, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CourseData( + course_key=course_id, + ), + ) + ) + def course_grade_now_failed(user, course_id): """ @@ -209,6 +257,45 @@ def course_grade_now_failed(user, course_id): } ) + # produce to event bus + if hasattr(course_id, 'ccx'): + CCX_COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CcxCoursePassingStatusData( + status=CcxCoursePassingStatusData.FAILING, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CcxCourseData( + ccx_course_key=course_id, + master_course_key=course_id.to_course_locator(), + ), + ) + ) + else: + COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CoursePassingStatusData( + status=CoursePassingStatusData.FAILING, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CourseData( + course_key=course_id, + ), + ) + ) + def fire_segment_event_on_course_grade_passed_first_time(user_id, course_locator): """ diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index b7843dfc1c1c..906b0d23ee00 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -4,16 +4,29 @@ from unittest import mock +from ccx_keys.locator import CCXLocator from django.utils.timezone import now from openedx_events.learning.data import ( + CcxCourseData, + CcxCoursePassingStatusData, CourseData, - PersistentCourseGradeData + CoursePassingStatusData, + PersistentCourseGradeData, + UserData, + UserPersonalData +) +from openedx_events.learning.signals import ( + CCX_COURSE_PASSING_STATUS_UPDATED, + COURSE_PASSING_STATUS_UPDATED, + PERSISTENT_GRADE_SUMMARY_CHANGED ) -from openedx_events.learning.signals import PERSISTENT_GRADE_SUMMARY_CHANGED from openedx_events.tests.utils import OpenEdxEventsTestMixin -from common.djangoapps.student.tests.factories import UserFactory +from common.djangoapps.student.tests.factories import AdminFactory, UserFactory +from lms.djangoapps.ccx.models import CustomCourseForEdX +from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.models import PersistentCourseGrade +from lms.djangoapps.grades.tests.utils import mock_passing_grade from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -94,5 +107,136 @@ def test_persistent_grade_event_emitted(self): passed_timestamp=grade.passed_timestamp ) }, - event_receiver.call_args.kwargs + event_receiver.call_args.kwargs, + ) + + +class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): # pylint: disable=missing-class-docstring + ENABLED_OPENEDX_EVENTS = [ + "org.openedx.learning.course.passing.status.updated.v1", + ] + + @classmethod + def setUpClass(cls): + """ + Set up class method for the Test class. + """ + super().setUpClass() + cls.start_events_isolation() + + def setUp(self): # pylint: disable=arguments-differ + super().setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create() + 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 + + def test_course_passing_status_updated_emitted(self): + event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) + COURSE_PASSING_STATUS_UPDATED.connect(event_receiver) + grade_factory = CourseGradeFactory() + + with mock_passing_grade(): + grade_factory.update(self.user, self.course) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": COURSE_PASSING_STATUS_UPDATED, + "sender": None, + "course_passing_status": CoursePassingStatusData( + status=CoursePassingStatusData.PASSING, + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.get_full_name(), + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + ), + ), + }, + event_receiver.call_args.kwargs, + ) + + +class CCXCoursePassingStatusEventsTest( # pylint: disable=missing-class-docstring + SharedModuleStoreTestCase, OpenEdxEventsTestMixin +): + ENABLED_OPENEDX_EVENTS = [ + "org.openedx.learning.ccx.course.passing.status.updated.v1", + ] + + @classmethod + def setUpClass(cls): + """ + Set up class method for the Test class. + """ + super().setUpClass() + cls.start_events_isolation() + + def setUp(self): # pylint: disable=arguments-differ + super().setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create() + self.coach = AdminFactory.create() + self.ccx = ccx = CustomCourseForEdX( + course_id=self.course.id, display_name="Test CCX", coach=self.coach + ) + ccx.save() + self.ccx_locator = CCXLocator.from_course_locator(self.course.id, ccx.id) + + 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 + + def test_ccx_course_passing_status_updated_emitted(self): + event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) + CCX_COURSE_PASSING_STATUS_UPDATED.connect(event_receiver) + grade_factory = CourseGradeFactory() + + with mock_passing_grade(): + grade_factory.update(self.user, self.store.get_course(self.ccx_locator)) + + self.assertTrue(self.receiver_called) + self.assertDictContainsSubset( + { + "signal": CCX_COURSE_PASSING_STATUS_UPDATED, + "sender": None, + "course_passing_status": CcxCoursePassingStatusData( + status=CcxCoursePassingStatusData.PASSING, + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.get_full_name(), + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CcxCourseData( + ccx_course_key=self.ccx_locator, + master_course_key=self.course.id, + display_name="", + coach_email="", + start=None, + end=None, + max_students_allowed=self.ccx.max_student_enrollments_allowed, + ), + ), + }, + event_receiver.call_args.kwargs, ) diff --git a/lms/envs/common.py b/lms/envs/common.py index 6acf880d4f4a..74c0e252afb4 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1076,6 +1076,15 @@ # .. toggle_warning: For consistency, keep the value in sync with the setting of the same name in the LMS and CMS. # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/34442 'ENABLE_BLAKE2B_HASHING': False, + + # .. toggle_name: FEATURES['BADGES_ENABLED'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Set to True to enable badges functionality. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2024-04-02 + # .. toggle_target_removal_date: None + 'BADGES_ENABLED': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API @@ -5447,7 +5456,12 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring def _should_send_certificate_events(settings): return settings.FEATURES['SEND_LEARNING_CERTIFICATE_LIFECYCLE_EVENTS_TO_BUS'] + #### Event bus producing #### + +def _should_send_learning_badge_events(settings): + return settings.FEATURES['BADGES_ENABLED'] + # .. setting_name: EVENT_BUS_PRODUCER_CONFIG # .. setting_default: all events disabled # .. setting_description: Dictionary of event_types mapped to dictionaries of topic to topic-related configuration. @@ -5520,11 +5534,37 @@ def _should_send_certificate_events(settings): 'course-authoring-xblock-lifecycle': {'event_key_field': 'xblock_info.usage_key', 'enabled': False}, }, + "org.openedx.learning.course.passing.status.updated.v1": { + "learning-badges-lifecycle": { + "event_key_field": "course_passing_status.course.course_key", + "enabled": _should_send_learning_badge_events, + }, + }, + "org.openedx.learning.ccx.course.passing.status.updated.v1": { + "learning-badges-lifecycle": { + "event_key_field": "course_passing_status.course.course_key", + "enabled": _should_send_learning_badge_events, + }, + }, } derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.created.v1', 'learning-certificate-lifecycle', 'enabled') derived_collection_entry('EVENT_BUS_PRODUCER_CONFIG', 'org.openedx.learning.certificate.revoked.v1', 'learning-certificate-lifecycle', 'enabled') + +derived_collection_entry( + "EVENT_BUS_PRODUCER_CONFIG", + "org.openedx.learning.course.passing.status.updated.v1", + "learning-badges-lifecycle", + "enabled", +) +derived_collection_entry( + "EVENT_BUS_PRODUCER_CONFIG", + "org.openedx.learning.ccx.course.passing.status.updated.v1", + "learning-badges-lifecycle", + "enabled", +) + BEAMER_PRODUCT_ID = "" #### Survey Report #### From f4674b40b346b5cfd6c43a632f434a5238a67841 Mon Sep 17 00:00:00 2001 From: andrii-hantkovskyi <131773947+andrii-hantkovskyi@users.noreply.github.com> Date: Tue, 7 May 2024 20:09:43 +0300 Subject: [PATCH 2/7] refactor: [ACI-972] extract event bus event to another function (#2552) Co-authored-by: Andrii --- lms/djangoapps/grades/event_utils.py | 49 ++++++++++++ lms/djangoapps/grades/events.py | 88 +--------------------- lms/djangoapps/grades/tests/test_events.py | 4 +- 3 files changed, 54 insertions(+), 87 deletions(-) create mode 100644 lms/djangoapps/grades/event_utils.py diff --git a/lms/djangoapps/grades/event_utils.py b/lms/djangoapps/grades/event_utils.py new file mode 100644 index 000000000000..8e4df28c846b --- /dev/null +++ b/lms/djangoapps/grades/event_utils.py @@ -0,0 +1,49 @@ +from openedx_events.learning.data import ( + CcxCourseData, + CcxCoursePassingStatusData, + CourseData, + CoursePassingStatusData, + UserData, + UserPersonalData +) +from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED + + +def emit_course_passing_status_update(user, course_id, is_passing): + if hasattr(course_id, 'ccx'): + CCX_COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CcxCoursePassingStatusData( + is_passing=is_passing, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CcxCourseData( + ccx_course_key=course_id, + master_course_key=course_id.to_course_locator(), + ), + ) + ) + else: + COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CoursePassingStatusData( + is_passing=is_passing, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CourseData( + course_key=course_id, + ), + ) + ) diff --git a/lms/djangoapps/grades/events.py b/lms/djangoapps/grades/events.py index 538f3b34eef3..aada530cb7d6 100644 --- a/lms/djangoapps/grades/events.py +++ b/lms/djangoapps/grades/events.py @@ -6,15 +6,6 @@ from crum import get_current_user from django.conf import settings from eventtracking import tracker -from openedx_events.learning.data import ( - CcxCourseData, - CcxCoursePassingStatusData, - CourseData, - CoursePassingStatusData, - UserData, - UserPersonalData -) -from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment @@ -25,6 +16,7 @@ get_event_transaction_type, set_event_transaction_type ) +from lms.djangoapps.grades.event_utils import emit_course_passing_status_update from lms.djangoapps.grades.signals.signals import SCHEDULE_FOLLOW_UP_SEGMENT_EVENT_FOR_COURSE_PASSED_FIRST_TIME from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.enterprise_support.context import get_enterprise_event_context @@ -199,44 +191,7 @@ def course_grade_now_passed(user, course_id): } ) - # produce to event bus - if hasattr(course_id, 'ccx'): - CCX_COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CcxCoursePassingStatusData( - status=CcxCoursePassingStatusData.PASSING, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CcxCourseData( - ccx_course_key=course_id, - master_course_key=course_id.to_course_locator(), - ), - ) - ) - else: - COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CoursePassingStatusData( - status=CoursePassingStatusData.PASSING, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CourseData( - course_key=course_id, - ), - ) - ) + emit_course_passing_status_update(user, course_id, is_passing=True) def course_grade_now_failed(user, course_id): @@ -257,44 +212,7 @@ def course_grade_now_failed(user, course_id): } ) - # produce to event bus - if hasattr(course_id, 'ccx'): - CCX_COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CcxCoursePassingStatusData( - status=CcxCoursePassingStatusData.FAILING, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CcxCourseData( - ccx_course_key=course_id, - master_course_key=course_id.to_course_locator(), - ), - ) - ) - else: - COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CoursePassingStatusData( - status=CoursePassingStatusData.FAILING, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CourseData( - course_key=course_id, - ), - ) - ) + emit_course_passing_status_update(user, course_id, is_passing=False) def fire_segment_event_on_course_grade_passed_first_time(user_id, course_locator): diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index 906b0d23ee00..aefe55fd423d 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -150,7 +150,7 @@ def test_course_passing_status_updated_emitted(self): "signal": COURSE_PASSING_STATUS_UPDATED, "sender": None, "course_passing_status": CoursePassingStatusData( - status=CoursePassingStatusData.PASSING, + is_passing=True, user=UserData( pii=UserPersonalData( username=self.user.username, @@ -217,7 +217,7 @@ def test_ccx_course_passing_status_updated_emitted(self): "signal": CCX_COURSE_PASSING_STATUS_UPDATED, "sender": None, "course_passing_status": CcxCoursePassingStatusData( - status=CcxCoursePassingStatusData.PASSING, + is_passing=True, user=UserData( pii=UserPersonalData( username=self.user.username, From e23f728db79899db64566bd73c9afdcfc000a73a Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Thu, 9 May 2024 09:19:51 +0300 Subject: [PATCH 3/7] refactor: add protected utility to send events and update docs --- lms/djangoapps/grades/event_utils.py | 49 --------------------- lms/djangoapps/grades/events.py | 66 +++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 56 deletions(-) delete mode 100644 lms/djangoapps/grades/event_utils.py diff --git a/lms/djangoapps/grades/event_utils.py b/lms/djangoapps/grades/event_utils.py deleted file mode 100644 index 8e4df28c846b..000000000000 --- a/lms/djangoapps/grades/event_utils.py +++ /dev/null @@ -1,49 +0,0 @@ -from openedx_events.learning.data import ( - CcxCourseData, - CcxCoursePassingStatusData, - CourseData, - CoursePassingStatusData, - UserData, - UserPersonalData -) -from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED - - -def emit_course_passing_status_update(user, course_id, is_passing): - if hasattr(course_id, 'ccx'): - CCX_COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CcxCoursePassingStatusData( - is_passing=is_passing, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CcxCourseData( - ccx_course_key=course_id, - master_course_key=course_id.to_course_locator(), - ), - ) - ) - else: - COURSE_PASSING_STATUS_UPDATED.send_event( - course_passing_status=CoursePassingStatusData( - is_passing=is_passing, - user=UserData( - pii=UserPersonalData( - username=user.username, - email=user.email, - name=user.get_full_name(), - ), - id=user.id, - is_active=user.is_active, - ), - course=CourseData( - course_key=course_id, - ), - ) - ) diff --git a/lms/djangoapps/grades/events.py b/lms/djangoapps/grades/events.py index aada530cb7d6..51d1b13702f0 100644 --- a/lms/djangoapps/grades/events.py +++ b/lms/djangoapps/grades/events.py @@ -6,6 +6,15 @@ from crum import get_current_user from django.conf import settings from eventtracking import tracker +from openedx_events.learning.data import ( + CcxCourseData, + CcxCoursePassingStatusData, + CourseData, + CoursePassingStatusData, + UserData, + UserPersonalData +) +from openedx_events.learning.signals import CCX_COURSE_PASSING_STATUS_UPDATED, COURSE_PASSING_STATUS_UPDATED from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.student.models import CourseEnrollment @@ -16,7 +25,6 @@ get_event_transaction_type, set_event_transaction_type ) -from lms.djangoapps.grades.event_utils import emit_course_passing_status_update from lms.djangoapps.grades.signals.signals import SCHEDULE_FOLLOW_UP_SEGMENT_EVENT_FOR_COURSE_PASSED_FIRST_TIME from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.enterprise_support.context import get_enterprise_event_context @@ -175,8 +183,8 @@ def course_grade_passed_first_time(user_id, course_id): def course_grade_now_passed(user, course_id): """ - Emits an edx.course.grade.now_passed event - with data from the course and user passed now . + Emits an edx.course.grade.now_passed and passing status updated events + with data from the course and user passed now. """ event_name = COURSE_GRADE_NOW_PASSED_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) @@ -191,13 +199,13 @@ def course_grade_now_passed(user, course_id): } ) - emit_course_passing_status_update(user, course_id, is_passing=True) + _emit_course_passing_status_update(user, course_id, is_passing=True) def course_grade_now_failed(user, course_id): """ - Emits an edx.course.grade.now_failed event - with data from the course and user failed now . + Emits an edx.course.grade.now_failed and passing status updated events + with data from the course and user failed now. """ event_name = COURSE_GRADE_NOW_FAILED_EVENT_TYPE context = contexts.course_context_from_course_id(course_id) @@ -212,7 +220,7 @@ def course_grade_now_failed(user, course_id): } ) - emit_course_passing_status_update(user, course_id, is_passing=False) + _emit_course_passing_status_update(user, course_id, is_passing=False) def fire_segment_event_on_course_grade_passed_first_time(user_id, course_locator): @@ -263,3 +271,47 @@ def fire_segment_event_on_course_grade_passed_first_time(user_id, course_locator ) log.info("Segment event fired for passed learners. Event: [{}], Data: [{}]".format(event_name, event_properties)) + + +def _emit_course_passing_status_update(user, course_id, is_passing): + """ + Emit course passing status event according to the course type. + The status of event is determined by is_passing parameter. + """ + if hasattr(course_id, 'ccx'): + CCX_COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CcxCoursePassingStatusData( + is_passing=is_passing, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CcxCourseData( + ccx_course_key=course_id, + master_course_key=course_id.to_course_locator(), + ), + ) + ) + else: + COURSE_PASSING_STATUS_UPDATED.send_event( + course_passing_status=CoursePassingStatusData( + is_passing=is_passing, + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.get_full_name(), + ), + id=user.id, + is_active=user.is_active, + ), + course=CourseData( + course_key=course_id, + ), + ) + ) From 1b244af2b2395601f5e15f39b157adc2e14d9f86 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Thu, 9 May 2024 10:09:14 +0300 Subject: [PATCH 4/7] chore: upgrade openedx-events dependency --- requirements/edx/base.txt | 3 ++- requirements/edx/development.txt | 3 ++- requirements/edx/doc.txt | 3 ++- requirements/edx/testing.txt | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 517f4a722e01..8f69054cb9d1 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -434,6 +434,7 @@ edx-ccx-keys==1.3.0 # via # -r requirements/edx/kernel.in # lti-consumer-xblock + # openedx-events edx-celeryutils==1.3.0 # via # -r requirements/edx/kernel.in @@ -780,7 +781,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.1.0 # via -r requirements/edx/kernel.in -openedx-events==9.9.2 +openedx-events==9.10.0 # via # -r requirements/edx/kernel.in # edx-event-bus-kafka diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 22da086ac200..132082235e68 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -698,6 +698,7 @@ edx-ccx-keys==1.3.0 # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt # lti-consumer-xblock + # openedx-events edx-celeryutils==1.3.0 # via # -r requirements/edx/doc.txt @@ -1298,7 +1299,7 @@ openedx-django-wiki==2.1.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt -openedx-events==9.9.2 +openedx-events==9.10.0 # via # -r requirements/edx/doc.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/doc.txt b/requirements/edx/doc.txt index 8d53f3852cb7..fe83f33bc65d 100644 --- a/requirements/edx/doc.txt +++ b/requirements/edx/doc.txt @@ -512,6 +512,7 @@ edx-ccx-keys==1.3.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock + # openedx-events edx-celeryutils==1.3.0 # via # -r requirements/edx/base.txt @@ -916,7 +917,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.1.0 # via -r requirements/edx/base.txt -openedx-events==9.9.2 +openedx-events==9.10.0 # via # -r requirements/edx/base.txt # edx-event-bus-kafka diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d640dc7fc5cd..de869074e97a 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -536,6 +536,7 @@ edx-ccx-keys==1.3.0 # via # -r requirements/edx/base.txt # lti-consumer-xblock + # openedx-events edx-celeryutils==1.3.0 # via # -r requirements/edx/base.txt @@ -971,7 +972,7 @@ openedx-django-require==2.1.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.1.0 # via -r requirements/edx/base.txt -openedx-events==9.9.2 +openedx-events==9.10.0 # via # -r requirements/edx/base.txt # edx-event-bus-kafka From 4cd9a564d1d3a64c35f589a78116416a9201c703 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Thu, 9 May 2024 16:36:24 +0300 Subject: [PATCH 5/7] fix: update code because of quality check --- cms/envs/common.py | 1 + lms/djangoapps/grades/tests/test_events.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index a777307089ec..c7f9d4bb5ab9 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2888,6 +2888,7 @@ def _should_send_xblock_events(settings): return settings.FEATURES['ENABLE_SEND_XBLOCK_LIFECYCLE_EVENTS_OVER_BUS'] + def _should_send_learning_badge_events(settings): return settings.FEATURES['BADGES_ENABLED'] diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index aefe55fd423d..bfce81925c5d 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -111,7 +111,7 @@ def test_persistent_grade_event_emitted(self): ) -class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): # pylint: disable=missing-class-docstring +class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): ENABLED_OPENEDX_EVENTS = [ "org.openedx.learning.course.passing.status.updated.v1", ] @@ -124,13 +124,13 @@ def setUpClass(cls): super().setUpClass() cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ + def setUp(self): super().setUp() self.course = CourseFactory.create() self.user = UserFactory.create() self.receiver_called = False - def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument + def _event_receiver_side_effect(self, **kwargs): """ Used show that the Open edX Event was called by the Django signal handler. """ @@ -169,7 +169,7 @@ def test_course_passing_status_updated_emitted(self): ) -class CCXCoursePassingStatusEventsTest( # pylint: disable=missing-class-docstring +class CCXCoursePassingStatusEventsTest( SharedModuleStoreTestCase, OpenEdxEventsTestMixin ): ENABLED_OPENEDX_EVENTS = [ @@ -184,7 +184,7 @@ def setUpClass(cls): super().setUpClass() cls.start_events_isolation() - def setUp(self): # pylint: disable=arguments-differ + def setUp(self): super().setUp() self.course = CourseFactory.create() self.user = UserFactory.create() @@ -197,7 +197,7 @@ def setUp(self): # pylint: disable=arguments-differ self.receiver_called = False - def _event_receiver_side_effect(self, **kwargs): # pylint: disable=unused-argument + def _event_receiver_side_effect(self, **kwargs): """ Used show that the Open edX Event was called by the Django signal handler. """ From 0d0d45eb4d9f4ca807f04e00cb7b6fc45303b400 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Thu, 9 May 2024 17:04:02 +0300 Subject: [PATCH 6/7] docs: add docstrings for tests --- lms/djangoapps/grades/tests/test_events.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lms/djangoapps/grades/tests/test_events.py b/lms/djangoapps/grades/tests/test_events.py index bfce81925c5d..eac8cc9a4a70 100644 --- a/lms/djangoapps/grades/tests/test_events.py +++ b/lms/djangoapps/grades/tests/test_events.py @@ -112,6 +112,9 @@ def test_persistent_grade_event_emitted(self): class CoursePassingStatusEventsTest(SharedModuleStoreTestCase, OpenEdxEventsTestMixin): + """ + Tests for Open edX passing status update event. + """ ENABLED_OPENEDX_EVENTS = [ "org.openedx.learning.course.passing.status.updated.v1", ] @@ -137,6 +140,9 @@ def _event_receiver_side_effect(self, **kwargs): self.receiver_called = True def test_course_passing_status_updated_emitted(self): + """ + Test whether passing status updated event is sent after the grade is being updated for a user. + """ event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) COURSE_PASSING_STATUS_UPDATED.connect(event_receiver) grade_factory = CourseGradeFactory() @@ -172,6 +178,9 @@ def test_course_passing_status_updated_emitted(self): class CCXCoursePassingStatusEventsTest( SharedModuleStoreTestCase, OpenEdxEventsTestMixin ): + """ + Tests for Open edX passing status update event in a CCX course. + """ ENABLED_OPENEDX_EVENTS = [ "org.openedx.learning.ccx.course.passing.status.updated.v1", ] @@ -204,6 +213,9 @@ def _event_receiver_side_effect(self, **kwargs): self.receiver_called = True def test_ccx_course_passing_status_updated_emitted(self): + """ + Test whether passing status updated event is sent after the grade is being updated in CCX course. + """ event_receiver = mock.Mock(side_effect=self._event_receiver_side_effect) CCX_COURSE_PASSING_STATUS_UPDATED.connect(event_receiver) grade_factory = CourseGradeFactory() From c14b5f543adca6facd5e333f5fde5def2de94035 Mon Sep 17 00:00:00 2001 From: Glib Glugovskiy Date: Thu, 9 May 2024 17:44:29 +0300 Subject: [PATCH 7/7] fix: correct course key for ccx event --- cms/envs/common.py | 3 ++- lms/envs/common.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index c7f9d4bb5ab9..e505f8245726 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -2892,6 +2892,7 @@ def _should_send_xblock_events(settings): def _should_send_learning_badge_events(settings): return settings.FEATURES['BADGES_ENABLED'] + # .. setting_name: EVENT_BUS_PRODUCER_CONFIG # .. setting_default: all events disabled # .. setting_description: Dictionary of event_types mapped to dictionaries of topic to topic-related configuration. @@ -2949,7 +2950,7 @@ def _should_send_learning_badge_events(settings): }, "org.openedx.learning.ccx.course.passing.status.updated.v1": { "learning-badges-lifecycle": { - "event_key_field": "course_passing_status.course.course_key", + "event_key_field": "course_passing_status.course.ccx_course_key", "enabled": _should_send_learning_badge_events, }, }, diff --git a/lms/envs/common.py b/lms/envs/common.py index 74c0e252afb4..5f8714ca9cf7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -5542,7 +5542,7 @@ def _should_send_learning_badge_events(settings): }, "org.openedx.learning.ccx.course.passing.status.updated.v1": { "learning-badges-lifecycle": { - "event_key_field": "course_passing_status.course.course_key", + "event_key_field": "course_passing_status.course.ccx_course_key", "enabled": _should_send_learning_badge_events, }, },