EDUCATOR-4082 | When creating subseciton grade override from service, create a PSG if one does not exist.

This commit is contained in:
Alex Dusenbery
2019-02-20 15:56:22 -05:00
committed by Alex Dusenbery
parent b2be81c675
commit 325c22c5d5
2 changed files with 109 additions and 33 deletions

View File

@@ -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)

View File

@@ -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)