EDUCATOR-4082 | When creating subseciton grade override from service, create a PSG if one does not exist.
This commit is contained in:
committed by
Alex Dusenbery
parent
b2be81c675
commit
325c22c5d5
@@ -3,9 +3,13 @@ Grade service
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
import pytz
|
||||
from six import text_type
|
||||
|
||||
from lms.djangoapps.courseware.courses import get_course
|
||||
from lms.djangoapps.grades.course_data import CourseData
|
||||
from lms.djangoapps.grades.subsection_grade import CreateSubsectionGrade
|
||||
from lms.djangoapps.utils import _get_key
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
|
||||
@@ -21,6 +25,9 @@ from .models import (
|
||||
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
|
||||
|
||||
|
||||
USER_MODEL = get_user_model()
|
||||
|
||||
|
||||
class GradesService(object):
|
||||
"""
|
||||
Course grade service
|
||||
@@ -55,21 +62,29 @@ class GradesService(object):
|
||||
|
||||
return PersistentSubsectionGradeOverride.get_override(user_id, usage_key)
|
||||
|
||||
def override_subsection_grade(self, user_id, course_key_or_id, usage_key_or_id, earned_all=None,
|
||||
earned_graded=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)
|
||||
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 PersistentSubsectionGrade table. Will not
|
||||
override earned_all or earned_graded value if they are None. Both default to None.
|
||||
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)
|
||||
|
||||
grade = PersistentSubsectionGrade.read_grade(
|
||||
user_id=user_id,
|
||||
usage_key=usage_key
|
||||
)
|
||||
try:
|
||||
grade = PersistentSubsectionGrade.read_grade(
|
||||
user_id=user_id,
|
||||
usage_key=usage_key
|
||||
)
|
||||
except PersistentSubsectionGrade.DoesNotExist:
|
||||
grade = self._create_subsection_grade(user_id, course_key, usage_key)
|
||||
|
||||
override = PersistentSubsectionGradeOverride.update_or_create_override(
|
||||
requesting_user=None,
|
||||
@@ -83,8 +98,8 @@ class GradesService(object):
|
||||
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.
|
||||
# This will eventually trigger a re-computation of the course grade,
|
||||
# taking the new PersistentSubsectionGradeOverride into account.
|
||||
SUBSECTION_OVERRIDE_CHANGED.send(
|
||||
sender=None,
|
||||
user_id=user_id,
|
||||
@@ -142,3 +157,17 @@ class GradesService(object):
|
||||
"""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)
|
||||
|
||||
def _create_subsection_grade(self, user_id, course_key, usage_key):
|
||||
"""
|
||||
Given a user_id, course_key, and subsection usage_key,
|
||||
creates a new ``PersistentSubsectionGrade``.
|
||||
"""
|
||||
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 = USER_MODEL.objects.get(id=user_id)
|
||||
course_data = CourseData(user, course=course)
|
||||
subsection_grade = CreateSubsectionGrade(subsection, course_data.structure, {}, {})
|
||||
return subsection_grade.update_or_create_model(user, force_update_subsections=True)
|
||||
|
||||
@@ -21,6 +21,9 @@ from ..constants import ScoreDatabaseTableEnum
|
||||
|
||||
|
||||
class MockWaffleFlag(object):
|
||||
"""
|
||||
A Mock WaffleFlag object.
|
||||
"""
|
||||
def __init__(self, state):
|
||||
self.state = state
|
||||
|
||||
@@ -40,6 +43,11 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
self.service = GradesService()
|
||||
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course', run='Spring2019')
|
||||
self.subsection = ItemFactory.create(parent=self.course, category="subsection", display_name="Subsection")
|
||||
self.subsection_without_grade = ItemFactory.create(
|
||||
parent=self.course,
|
||||
category="subsection",
|
||||
display_name="Subsection without grade"
|
||||
)
|
||||
self.user = UserFactory()
|
||||
self.grade = PersistentSubsectionGrade.update_or_create_grade(
|
||||
user_id=self.user.id,
|
||||
@@ -66,6 +74,7 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
super(GradesServiceTests, self).tearDown()
|
||||
PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides
|
||||
self.signal_patcher.stop()
|
||||
self.id_patcher.stop()
|
||||
@@ -140,37 +149,24 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
self.assertEqual(override_history.action, history_action)
|
||||
|
||||
@ddt.data(
|
||||
[{
|
||||
{
|
||||
'earned_all': 0.0,
|
||||
'earned_graded': 0.0
|
||||
}, {
|
||||
'earned_all': 0.0,
|
||||
'earned_graded': 0.0
|
||||
}],
|
||||
[{
|
||||
},
|
||||
{
|
||||
'earned_all': 0.0,
|
||||
'earned_graded': None
|
||||
}, {
|
||||
'earned_all': 0.0,
|
||||
'earned_graded': 5.0
|
||||
}],
|
||||
[{
|
||||
},
|
||||
{
|
||||
'earned_all': None,
|
||||
'earned_graded': None
|
||||
}, {
|
||||
'earned_all': 6.0,
|
||||
'earned_graded': 5.0
|
||||
}],
|
||||
[{
|
||||
},
|
||||
{
|
||||
'earned_all': 3.0,
|
||||
'earned_graded': 2.0
|
||||
}, {
|
||||
'earned_all': 3.0,
|
||||
'earned_graded': 2.0
|
||||
}],
|
||||
},
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_override_subsection_grade(self, override, expected):
|
||||
def test_override_subsection_grade(self, override):
|
||||
self.service.override_subsection_grade(
|
||||
user_id=self.user.id,
|
||||
course_key_or_id=self.course.id,
|
||||
@@ -204,6 +200,57 @@ class GradesServiceTests(ModuleStoreTestCase):
|
||||
override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_obj.id).first()
|
||||
self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE)
|
||||
|
||||
def test_override_subsection_grade_no_psg(self):
|
||||
"""
|
||||
When there is no PersistentSubsectionGrade associated with the learner
|
||||
and subsection to override, one should be created.
|
||||
"""
|
||||
earned_all_override = 2
|
||||
earned_graded_override = 0
|
||||
self.service.override_subsection_grade(
|
||||
user_id=self.user.id,
|
||||
course_key_or_id=self.course.id,
|
||||
usage_key_or_id=self.subsection_without_grade.location,
|
||||
earned_all=earned_all_override,
|
||||
earned_graded=earned_graded_override
|
||||
)
|
||||
|
||||
# Assert that a new PersistentSubsectionGrade was created
|
||||
subsection_grade = self.service.get_subsection_grade(
|
||||
self.user.id,
|
||||
self.course.id,
|
||||
self.subsection_without_grade.location
|
||||
)
|
||||
self.assertIsNotNone(subsection_grade)
|
||||
self.assertEqual(0, subsection_grade.earned_all)
|
||||
self.assertEqual(0, subsection_grade.earned_graded)
|
||||
|
||||
# Now assert things about the grade override
|
||||
override_obj = self.service.get_subsection_grade_override(
|
||||
self.user.id,
|
||||
self.course.id,
|
||||
self.subsection_without_grade.location
|
||||
)
|
||||
self.assertIsNotNone(override_obj)
|
||||
self.assertEqual(override_obj.earned_all_override, earned_all_override)
|
||||
self.assertEqual(override_obj.earned_graded_override, earned_graded_override)
|
||||
|
||||
self.assertEqual(
|
||||
self.mock_signal.call_args,
|
||||
call(
|
||||
sender=None,
|
||||
user_id=self.user.id,
|
||||
course_id=unicode(self.course.id),
|
||||
usage_id=unicode(self.subsection_without_grade.location),
|
||||
only_if_higher=False,
|
||||
modified=override_obj.modified,
|
||||
score_deleted=False,
|
||||
score_db_table=ScoreDatabaseTableEnum.overrides
|
||||
)
|
||||
)
|
||||
override_history = PersistentSubsectionGradeOverrideHistory.objects.filter(override_id=override_obj.id).first()
|
||||
self._verify_override_history(override_history, PersistentSubsectionGradeOverrideHistory.CREATE_OR_UPDATE)
|
||||
|
||||
@freeze_time('2017-01-01')
|
||||
def test_undo_override_subsection_grade(self):
|
||||
override, _ = PersistentSubsectionGradeOverride.objects.update_or_create(grade=self.grade)
|
||||
|
||||
Reference in New Issue
Block a user