157 lines
6.7 KiB
Python
157 lines
6.7 KiB
Python
# pylint: disable=unused-import,wildcard-import
|
|
"""
|
|
Python APIs exposed by the grades app to other in-process apps.
|
|
"""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
from datetime import datetime
|
|
|
|
import pytz
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from opaque_keys.edx.keys import CourseKey, UsageKey
|
|
from six import text_type
|
|
|
|
# Public Grades Modules
|
|
from lms.djangoapps.grades import constants, context, course_data, events
|
|
# Grades APIs that should NOT belong within the Grades subsystem
|
|
# TODO move Gradebook to be an external feature outside of core Grades
|
|
from lms.djangoapps.grades.config.waffle import is_writable_gradebook_enabled, gradebook_can_see_bulk_management
|
|
# Public Grades Factories
|
|
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
|
from lms.djangoapps.grades.models_api import *
|
|
from lms.djangoapps.grades.signals import signals
|
|
# TODO exposing functionality from Grades handlers seems fishy.
|
|
from lms.djangoapps.grades.signals.handlers import disconnect_submissions_signal_receiver
|
|
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
|
from lms.djangoapps.grades.subsection_grade_factory import SubsectionGradeFactory
|
|
from lms.djangoapps.grades.tasks import compute_all_grades_for_course as task_compute_all_grades_for_course
|
|
from lms.djangoapps.grades.util_services import GradesUtilService
|
|
from lms.djangoapps.utils import _get_key
|
|
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
|
|
|
|
|
def graded_subsections_for_course_id(course_id):
|
|
"""
|
|
Return graded subsections for the course.
|
|
"""
|
|
from lms.djangoapps.grades.context import graded_subsections_for_course
|
|
return graded_subsections_for_course(course_data.CourseData(user=None, course_key=course_id).collected_structure)
|
|
|
|
|
|
def override_subsection_grade(
|
|
user_id, course_key_or_id, usage_key_or_id, overrider=None, earned_all=None, earned_graded=None,
|
|
feature=constants.GradeOverrideFeatureEnum.proctoring, comment=None,
|
|
):
|
|
"""
|
|
Creates a PersistentSubsectionGradeOverride corresponding to the given
|
|
user, course, and usage_key.
|
|
Will also create a ``PersistentSubsectionGrade`` for this (user, course, usage_key)
|
|
if none currently exists.
|
|
|
|
Fires off a recalculate_subsection_grade async task to update the PersistentCourseGrade table.
|
|
Will not override ``earned_all`` or ``earned_graded`` value if they are ``None``.
|
|
Both of these parameters have ``None`` as their default value.
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
try:
|
|
grade = get_subsection_grade(user_id, usage_key.course_key, usage_key)
|
|
except ObjectDoesNotExist:
|
|
grade = _create_subsection_grade(user_id, course_key, usage_key)
|
|
|
|
override = update_or_create_override(
|
|
grade,
|
|
requesting_user=overrider,
|
|
subsection_grade_model=grade,
|
|
feature=feature,
|
|
system=feature,
|
|
earned_all_override=earned_all,
|
|
earned_graded_override=earned_graded,
|
|
comment=comment,
|
|
)
|
|
|
|
# Cache a new event id and event type which the signal handler will use to emit a tracking log event.
|
|
create_new_event_transaction_id()
|
|
set_event_transaction_type(events.SUBSECTION_OVERRIDE_EVENT_TYPE)
|
|
|
|
# This will eventually trigger a re-computation of the course grade,
|
|
# taking the new PersistentSubsectionGradeOverride into account.
|
|
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
|
sender=None,
|
|
user_id=user_id,
|
|
course_id=text_type(course_key),
|
|
usage_id=text_type(usage_key),
|
|
only_if_higher=False,
|
|
modified=override.modified,
|
|
score_deleted=False,
|
|
score_db_table=constants.ScoreDatabaseTableEnum.overrides
|
|
)
|
|
|
|
|
|
def undo_override_subsection_grade(user_id, course_key_or_id, usage_key_or_id, feature=''):
|
|
"""
|
|
Delete the override subsection grade row (the PersistentSubsectionGrade model must already exist)
|
|
|
|
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. If the
|
|
override does not exist, no error is raised, it just triggers the recalculation.
|
|
|
|
feature: if specified, the deletion will only occur if the
|
|
override to be deleted was created by the corresponding
|
|
subsystem
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
try:
|
|
override = get_subsection_grade_override(user_id, course_key, usage_key)
|
|
except ObjectDoesNotExist:
|
|
return
|
|
|
|
if override is not None and (
|
|
not feature or not override.system or feature == override.system):
|
|
override.delete()
|
|
else:
|
|
return
|
|
|
|
# Cache a new event id and event type which the signal handler will use to emit a tracking log event.
|
|
create_new_event_transaction_id()
|
|
set_event_transaction_type(events.SUBSECTION_OVERRIDE_EVENT_TYPE)
|
|
|
|
# Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
|
|
# which will no longer use the above deleted override, and instead return the grade to the original score from
|
|
# the actual problem responses before writing to the table.
|
|
signals.SUBSECTION_OVERRIDE_CHANGED.send(
|
|
sender=None,
|
|
user_id=user_id,
|
|
course_id=text_type(course_key),
|
|
usage_id=text_type(usage_key),
|
|
only_if_higher=False,
|
|
modified=datetime.now().replace(tzinfo=pytz.UTC), # Not used when score_deleted=True
|
|
score_deleted=True,
|
|
score_db_table=constants.ScoreDatabaseTableEnum.overrides
|
|
)
|
|
|
|
|
|
def should_override_grade_on_rejected_exam(course_key_or_id):
|
|
"""Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
|
|
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
|
|
|
|
|
|
def _create_subsection_grade(user_id, course_key, usage_key):
|
|
"""
|
|
Given a user_id, course_key, and subsection usage_key,
|
|
creates a new ``PersistentSubsectionGrade``.
|
|
"""
|
|
from lms.djangoapps.courseware.courses import get_course
|
|
from django.contrib.auth import get_user_model
|
|
course = get_course(course_key, depth=None)
|
|
subsection = course.get_child(usage_key)
|
|
if not subsection:
|
|
raise Exception('Subsection with given usage_key does not exist.')
|
|
user = get_user_model().objects.get(id=user_id)
|
|
subsection_grade = CreateSubsectionGrade(subsection, course_data.CourseData(user, course=course).structure, {}, {})
|
|
return subsection_grade.update_or_create_model(user, force_update_subsections=True)
|