Files
edx-platform/lms/djangoapps/grades/events.py
Glib Glugovskiy 4599e45b2e feat: emit passing status updated events for badging (#34749)
Introduce emission of the COURSE_PASSING_STATUS_UPDATED as well as CCX_COURSE_PASSING_STATUS_UPDATED events, that are groundwork for the new Credly integration and the future badging initiative.

Product GH ticket for tracking - openedx/platform-roadmap#280
2024-05-09 11:52:36 -04:00

318 lines
12 KiB
Python

"""
Emits course grade events.
"""
from logging import getLogger
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
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 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
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'
PROBLEM_SUBMITTED_EVENT_TYPE = 'edx.grades.problem.submitted'
STATE_DELETED_EVENT_TYPE = 'edx.grades.problem.state_deleted'
SUBSECTION_OVERRIDE_EVENT_TYPE = 'edx.grades.subsection.score_overridden'
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'
def grade_updated(**kwargs):
"""
Emits the appropriate grade-related event after checking for which
event-transaction is active.
Emits a problem.submitted event only if there is no current event
transaction type, i.e. we have not reached this point in the code via
an outer event type (such as problem.rescored or score_overridden).
"""
root_type = get_event_transaction_type()
if not root_type:
root_id = get_event_transaction_id()
if not root_id:
root_id = create_new_event_transaction_id()
set_event_transaction_type(PROBLEM_SUBMITTED_EVENT_TYPE)
tracker.emit(
str(PROBLEM_SUBMITTED_EVENT_TYPE),
{
'user_id': str(kwargs['user_id']),
'course_id': str(kwargs['course_id']),
'problem_id': str(kwargs['usage_id']),
'event_transaction_id': str(root_id),
'event_transaction_type': str(PROBLEM_SUBMITTED_EVENT_TYPE),
'weighted_earned': kwargs.get('weighted_earned'),
'weighted_possible': kwargs.get('weighted_possible'),
}
)
elif root_type in [GRADES_RESCORE_EVENT_TYPE, GRADES_OVERRIDE_EVENT_TYPE]:
current_user = get_current_user()
instructor_id = getattr(current_user, 'id', None)
tracker.emit(
str(root_type),
{
'course_id': str(kwargs['course_id']),
'user_id': str(kwargs['user_id']),
'problem_id': str(kwargs['usage_id']),
'new_weighted_earned': kwargs.get('weighted_earned'),
'new_weighted_possible': kwargs.get('weighted_possible'),
'only_if_higher': kwargs.get('only_if_higher'),
'instructor_id': str(instructor_id),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(root_type),
}
)
elif root_type in [SUBSECTION_OVERRIDE_EVENT_TYPE]:
tracker.emit(
str(root_type),
{
'course_id': str(kwargs['course_id']),
'user_id': str(kwargs['user_id']),
'problem_id': str(kwargs['usage_id']),
'only_if_higher': kwargs.get('only_if_higher'),
'override_deleted': kwargs.get('score_deleted', False),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(root_type),
}
)
def subsection_grade_calculated(subsection_grade):
"""
Emits an edx.grades.subsection.grade_calculated event
with data from the passed subsection_grade.
"""
event_name = SUBSECTION_GRADE_CALCULATED
context = contexts.course_context_from_course_id(subsection_grade.course_id)
# TODO (AN-6134): remove this context manager
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': str(subsection_grade.user_id),
'course_id': str(subsection_grade.course_id),
'block_id': str(subsection_grade.usage_key),
'course_version': str(subsection_grade.course_version),
'weighted_total_earned': subsection_grade.earned_all,
'weighted_total_possible': subsection_grade.possible_all,
'weighted_graded_earned': subsection_grade.earned_graded,
'weighted_graded_possible': subsection_grade.possible_graded,
'first_attempted': str(subsection_grade.first_attempted),
'subtree_edited_timestamp': str(subsection_grade.subtree_edited_timestamp),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(get_event_transaction_type()),
'visible_blocks_hash': str(subsection_grade.visible_blocks_id),
}
)
def course_grade_calculated(course_grade):
"""
Emits an edx.grades.course.grade_calculated event
with data from the passed course_grade.
"""
event_name = COURSE_GRADE_CALCULATED
context = contexts.course_context_from_course_id(course_grade.course_id)
# TODO (AN-6134): remove this context manager
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': str(course_grade.user_id),
'course_id': str(course_grade.course_id),
'course_version': str(course_grade.course_version),
'percent_grade': course_grade.percent_grade,
'letter_grade': str(course_grade.letter_grade),
'course_edited_timestamp': str(course_grade.course_edited_timestamp),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(get_event_transaction_type()),
'grading_policy_hash': str(course_grade.grading_policy_hash),
}
)
def course_grade_passed_first_time(user_id, course_id):
"""
Emits an event edx.course.grade.passed.first_time
with data from the passed course_grade.
"""
event_name = COURSE_GRADE_PASSED_FIRST_TIME_EVENT_TYPE
context = contexts.course_context_from_course_id(course_id)
context_enterprise = get_enterprise_event_context(user_id, course_id)
context.update(context_enterprise)
# TODO (AN-6134): remove this context manager
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': str(user_id),
'course_id': str(course_id),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(get_event_transaction_type())
}
)
def course_grade_now_passed(user, course_id):
"""
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)
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': str(user.id),
'course_id': str(course_id),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(get_event_transaction_type())
}
)
_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 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)
with tracker.get_tracker().context(event_name, context):
tracker.emit(
event_name,
{
'user_id': str(user.id),
'course_id': str(course_id),
'event_transaction_id': str(get_event_transaction_id()),
'event_transaction_type': str(get_event_transaction_type())
}
)
_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):
"""
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 = 'edx.course.learner.passed.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:
enrollment = 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_ENROLLMENT_ID': enrollment.id,
'COURSE_TITLE': courserun_display_name,
'COURSE_ORG_NAME': courserun_org,
}
if getattr(settings, 'OUTCOME_SURVEYS_EVENTS_ENABLED', False):
segment.track(user_id, event_name, event_properties)
# fire signal so that a follow up event can be scheduled in outcome_surveys app
SCHEDULE_FOLLOW_UP_SEGMENT_EVENT_FOR_COURSE_PASSED_FIRST_TIME.send(
sender=None,
user_id=user_id,
course_id=course_locator,
event_properties=event_properties
)
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,
),
)
)