From cc0690fcda7fd04aa516533cab1e66a3c1ac282c Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Tue, 24 May 2022 01:06:39 +0500 Subject: [PATCH] feat: send segment event for learners who passed a course first time --- lms/djangoapps/grades/events.py | 50 ++++++++++++++- lms/djangoapps/grades/signals/handlers.py | 1 + .../grades/tests/test_course_grade_factory.py | 2 +- lms/djangoapps/grades/tests/test_signals.py | 62 ++++++++++++++++--- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/grades/events.py b/lms/djangoapps/grades/events.py index da7750cebb..db048303e1 100644 --- a/lms/djangoapps/grades/events.py +++ b/lms/djangoapps/grades/events.py @@ -1,19 +1,25 @@ """ Emits course grade events. """ +from logging import getLogger + from crum import get_current_user from eventtracking import tracker -from common.djangoapps.track import contexts +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.track import contexts, segment from common.djangoapps.track.event_transaction_utils import ( create_new_event_transaction_id, get_event_transaction_id, get_event_transaction_type, set_event_transaction_type ) - +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.features.enterprise_support.context import get_enterprise_event_context +log = getLogger(__name__) + COURSE_GRADE_CALCULATED = 'edx.grades.course.grade_calculated' GRADES_OVERRIDE_EVENT_TYPE = 'edx.grades.problem.score_overridden' GRADES_RESCORE_EVENT_TYPE = 'edx.grades.problem.rescored' @@ -24,6 +30,7 @@ SUBSECTION_GRADE_CALCULATED = 'edx.grades.subsection.grade_calculated' COURSE_GRADE_PASSED_FIRST_TIME_EVENT_TYPE = 'edx.course.grade.passed.first_time' COURSE_GRADE_NOW_PASSED_EVENT_TYPE = 'edx.course.grade.now_passed' COURSE_GRADE_NOW_FAILED_EVENT_TYPE = 'edx.course.grade.now_failed' +LEARNER_PASSED_COURSE_FIRST_TIME = 'edx.course.learner.passed.first_time' def grade_updated(**kwargs): @@ -200,3 +207,42 @@ def course_grade_now_failed(user, course_id): 'event_transaction_type': str(get_event_transaction_type()) } ) + + +def fire_segment_event_on_course_grade_passed_first_time(user_id, course_locator): + """ + Fire a segment event `edx.course.grade.passed.first_time` with the desired data. + + * Event should be only fired for learners enrolled in paid enrollment modes. + """ + event_name = LEARNER_PASSED_COURSE_FIRST_TIME + courserun_key = str(course_locator) + courserun_org = course_locator.org + paid_enrollment_modes = ( + CourseMode.MASTERS, + CourseMode.VERIFIED, + CourseMode.CREDIT_MODE, + CourseMode.PROFESSIONAL, + CourseMode.NO_ID_PROFESSIONAL_MODE, + ) + + try: + __ = CourseEnrollment.objects.get(user_id=user_id, course_id=courserun_key, mode__in=paid_enrollment_modes) + except CourseEnrollment.DoesNotExist: + return + + try: + courserun_display_name = CourseOverview.objects.values_list('display_name', flat=True).get(id=courserun_key) + except CourseOverview.DoesNotExist: + return + + event_properties = { + 'LMS_USER_ID': user_id, + 'COURSERUN_KEY': courserun_key, + 'COURSE_TITLE': courserun_display_name, + 'COURSE_ORG_NAME': courserun_org, + 'PASSED': 1, + } + segment.track(user_id, event_name, event_properties) + + log.info("Segment event fired for passed learners. Event: [{}], Data: [{}]".format(event_name, event_properties)) diff --git a/lms/djangoapps/grades/signals/handlers.py b/lms/djangoapps/grades/signals/handlers.py index 03176632b9..af33279515 100644 --- a/lms/djangoapps/grades/signals/handlers.py +++ b/lms/djangoapps/grades/signals/handlers.py @@ -299,3 +299,4 @@ def listen_for_course_grade_passed_first_time(sender, user_id, course_id, **kwar Emits an event edx.course.grade.passed.first_time """ events.course_grade_passed_first_time(user_id, course_id) + events.fire_segment_event_on_course_grade_passed_first_time(user_id, course_id) diff --git a/lms/djangoapps/grades/tests/test_course_grade_factory.py b/lms/djangoapps/grades/tests/test_course_grade_factory.py index df9acb4905..39b2cfb60c 100644 --- a/lms/djangoapps/grades/tests/test_course_grade_factory.py +++ b/lms/djangoapps/grades/tests/test_course_grade_factory.py @@ -130,7 +130,7 @@ class TestCourseGradeFactory(GradeTestBase): with self.assertNumQueries(4), mock_get_score(1, 2): _assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0 - num_queries = 41 + num_queries = 42 with self.assertNumQueries(num_queries), mock_get_score(1, 2): grade_factory.update(self.request.user, self.course, force_update_subsections=True) diff --git a/lms/djangoapps/grades/tests/test_signals.py b/lms/djangoapps/grades/tests/test_signals.py index 4694c24299..8c161d86ac 100644 --- a/lms/djangoapps/grades/tests/test_signals.py +++ b/lms/djangoapps/grades/tests/test_signals.py @@ -11,21 +11,25 @@ import ddt import pytest import pytz from django.test import TestCase -from submissions.models import score_reset, score_set from opaque_keys.edx.locator import CourseLocator +from submissions.models import score_reset, score_set +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type from common.djangoapps.util.date_utils import to_timestamp +from lms.djangoapps.grades.models import PersistentCourseGrade +from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from ..constants import ScoreDatabaseTableEnum from ..signals.handlers import ( disconnect_submissions_signal_receiver, + listen_for_course_grade_passed_first_time, + listen_for_failing_grade, + listen_for_passing_grade, problem_raw_score_changed_handler, submissions_score_reset_handler, - submissions_score_set_handler, - listen_for_course_grade_passed_first_time, - listen_for_passing_grade, - listen_for_failing_grade + submissions_score_set_handler ) from ..signals.signals import PROBLEM_RAW_SCORE_CHANGED @@ -266,7 +270,7 @@ class ScoreChangedSignalRelayTest(TestCase): pass -class CourseEventsSignalsTest(TestCase): +class CourseEventsSignalsTest(ModuleStoreTestCase): """ Tests to ensure that the courseware module correctly catches course grades passed/failed signal and emit course related event @@ -281,10 +285,6 @@ class CourseEventsSignalsTest(TestCase): Configure mocks for all the dependencies of the render method """ super().setUp() - self.signal_mock = self.setup_patch( - 'lms.djangoapps.grades.signals.signals.COURSE_GRADE_PASSED_FIRST_TIME.send', - None, - ) self.user_mock = MagicMock() self.user_mock.id = 42 self.get_user_mock = self.setup_patch( @@ -296,6 +296,8 @@ class CourseEventsSignalsTest(TestCase): course='some_course', run='some_run' ) + self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") + self.client.login(username=self.user.username, password="edx") def setup_patch(self, function_name, return_value): """ @@ -385,3 +387,43 @@ class CourseEventsSignalsTest(TestCase): 'event_transaction_type': str(get_event_transaction_type()), } ) + + @patch('lms.djangoapps.grades.events.segment.track') + def test_segment_event_on_course_grade_passed_first_time(self, segment_track_mock): + course = CourseOverviewFactory() + __ = CourseEnrollmentFactory( + is_active=True, + mode='verified', + course=course, + user=self.user + ) + params = { + "user_id": self.user.id, + "course_id": course.id, + "course_version": "JoeMcEwing", + "course_edited_timestamp": datetime( + year=2016, + month=8, + day=1, + hour=18, + minute=53, + second=24, + microsecond=354741, + tzinfo=pytz.UTC, + ), + "percent_grade": 77.7, + "letter_grade": "Great job", + "passed": True, + } + __ = PersistentCourseGrade.update_or_create(**params) + segment_track_mock.assert_called_with( + self.user.id, + 'edx.course.learner.passed.first_time', + { + 'LMS_USER_ID': self.user.id, + 'COURSERUN_KEY': str(course.id), + 'COURSE_TITLE': course.display_name, + 'COURSE_ORG_NAME': course.org, + 'PASSED': 1, + } + )