143 lines
5.9 KiB
Python
143 lines
5.9 KiB
Python
from datetime import datetime
|
|
|
|
import pytz
|
|
|
|
from opaque_keys.edx.keys import CourseKey, UsageKey
|
|
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
|
|
|
from .config.waffle import waffle_flags, REJECTED_EXAM_OVERRIDES_GRADE
|
|
from .constants import ScoreDatabaseTableEnum
|
|
from .events import SUBSECTION_OVERRIDE_EVENT_TYPE
|
|
from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
|
|
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
|
|
|
|
|
|
def _get_key(key_or_id, key_cls):
|
|
"""
|
|
Helper method to get a course/usage key either from a string or a key_cls,
|
|
where the key_cls (CourseKey or UsageKey) will simply be returned.
|
|
"""
|
|
return (
|
|
key_cls.from_string(key_or_id)
|
|
if isinstance(key_or_id, basestring)
|
|
else key_or_id
|
|
)
|
|
|
|
|
|
class GradesService(object):
|
|
"""
|
|
Course grade service
|
|
|
|
Provides various functions related to getting, setting, and overriding user grades.
|
|
"""
|
|
|
|
def get_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
|
|
"""
|
|
Finds and returns the earned subsection grade for user
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
return PersistentSubsectionGrade.objects.get(
|
|
user_id=user_id,
|
|
course_id=course_key,
|
|
usage_key=usage_key
|
|
)
|
|
|
|
def get_subsection_grade_override(self, user_id, course_key_or_id, usage_key_or_id):
|
|
"""
|
|
Finds the subsection grade for user and returns the override for that grade if it exists
|
|
|
|
If override does not exist, returns None. If subsection grade does not exist, will raise an exception.
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
grade = self.get_subsection_grade(user_id, course_key, usage_key)
|
|
|
|
try:
|
|
return PersistentSubsectionGradeOverride.objects.get(
|
|
grade=grade
|
|
)
|
|
except PersistentSubsectionGradeOverride.DoesNotExist:
|
|
return None
|
|
|
|
def override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id, earned_all=None,
|
|
earned_graded=None):
|
|
"""
|
|
Override subsection grade (the PersistentSubsectionGrade model must already exist)
|
|
|
|
Fires off a recalculate_subsection_grade async task to update the PersistentSubsectionGrade table. Will not
|
|
override earned_all or earned_graded value if they are None. Both default to None.
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
grade = PersistentSubsectionGrade.objects.get(
|
|
user_id=user_id,
|
|
course_id=course_key,
|
|
usage_key=usage_key
|
|
)
|
|
|
|
# Create override that will prevent any future updates to grade
|
|
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(
|
|
grade=grade,
|
|
earned_all_override=earned_all,
|
|
earned_graded_override=earned_graded
|
|
)
|
|
|
|
# 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(SUBSECTION_OVERRIDE_EVENT_TYPE)
|
|
|
|
# Signal will trigger subsection recalculation which will call PersistentSubsectionGrade.update_or_create_grade
|
|
# which will use the above override to update the grade before writing to the table.
|
|
SUBSECTION_OVERRIDE_CHANGED.send(
|
|
sender=None,
|
|
user_id=user_id,
|
|
course_id=unicode(course_key),
|
|
usage_id=unicode(usage_key),
|
|
only_if_higher=False,
|
|
modified=override.modified,
|
|
score_deleted=False,
|
|
score_db_table=ScoreDatabaseTableEnum.overrides
|
|
)
|
|
|
|
def undo_override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id):
|
|
"""
|
|
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.
|
|
"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
usage_key = _get_key(usage_key_or_id, UsageKey)
|
|
|
|
override = self.get_subsection_grade_override(user_id, course_key, usage_key)
|
|
# Older rejected exam attempts that transition to verified might not have an override created
|
|
if override is not None:
|
|
override.delete()
|
|
|
|
# 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(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.
|
|
SUBSECTION_OVERRIDE_CHANGED.send(
|
|
sender=None,
|
|
user_id=user_id,
|
|
course_id=unicode(course_key),
|
|
usage_id=unicode(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=ScoreDatabaseTableEnum.overrides
|
|
)
|
|
|
|
def should_override_grade_on_rejected_exam(self, course_key_or_id):
|
|
"""Convienence function to return the state of the CourseWaffleFlag REJECTED_EXAM_OVERRIDES_GRADE"""
|
|
course_key = _get_key(course_key_or_id, CourseKey)
|
|
return waffle_flags()[REJECTED_EXAM_OVERRIDES_GRADE].is_enabled(course_key)
|