Commit 4c353d93 by Tyler Hallada

Create GradesService to override persistent grades

parent f86f5998
......@@ -411,6 +411,24 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
user_id = params.pop('user_id')
usage_key = params.pop('usage_key')
# apply grade override if one exists before saving model
try:
override = PersistentSubsectionGradeOverride.objects.get(
grade__user_id=user_id,
grade__course_id=usage_key.course_key,
grade__usage_key=usage_key,
)
if override.earned_all_override is not None:
params['earned_all'] = override.earned_all_override
if override.possible_all_override is not None:
params['possible_all'] = override.possible_all_override
if override.earned_graded_override is not None:
params['earned_graded'] = override.earned_graded_override
if override.possible_graded_override is not None:
params['possible_graded'] = override.possible_graded_override
except PersistentSubsectionGradeOverride.DoesNotExist:
pass
grade, _ = cls.objects.update_or_create(
user_id=user_id,
course_id=usage_key.course_key,
......@@ -666,3 +684,20 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel):
'grading_policy_hash': unicode(grade.grading_policy_hash),
}
)
class PersistentSubsectionGradeOverride(models.Model):
"""
A django model tracking persistent grades overrides at the subsection level.
"""
class Meta(object):
app_label = "grades"
grade = models.OneToOneField(PersistentSubsectionGrade, related_name='override')
# earned/possible refers to the number of points achieved and available to achieve.
# graded refers to the subset of all problems that are marked as being graded.
earned_all_override = models.FloatField(null=True, blank=True)
possible_all_override = models.FloatField(null=True, blank=True)
earned_graded_override = models.FloatField(null=True, blank=True)
possible_graded_override = models.FloatField(null=True, blank=True)
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
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, subsection):
"""
Finds and returns the earned subsection grade for user
Result is a dict of two key value pairs with keys: earned_all and earned_graded.
"""
grade = PersistentSubsectionGrade.objects.get(
user_id=user_id,
course_id=course_key_or_id,
usage_key=subsection
)
return {
'earned_all': grade.earned_all,
'earned_graded': grade.earned_graded
}
def override_subsection_grade(self, user_id, course_key_or_id, subsection, earned_all=None, earned_graded=None):
"""
Override subsection grade (the PersistentSubsectionGrade model must already exist)
Will not override earned_all or earned_graded value if they are None. Both default to None.
"""
grade = PersistentSubsectionGrade.objects.get(
user_id=user_id,
course_id=course_key_or_id,
usage_key=subsection
)
# Create override that will prevent any future updates to grade
PersistentSubsectionGradeOverride.objects.create(
grade=grade,
earned_all_override=earned_all,
earned_graded_override=earned_graded
)
# Change the grade as it is now
if earned_all is not None:
grade.earned_all = earned_all
if earned_graded is not None:
grade.earned_graded = earned_graded
grade.save()
......@@ -23,6 +23,7 @@ from lms.djangoapps.grades.models import (
BlockRecordList,
PersistentCourseGrade,
PersistentSubsectionGrade,
PersistentSubsectionGradeOverride,
VisibleBlocks
)
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
......@@ -306,6 +307,14 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
grade = PersistentSubsectionGrade.create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade)
def test_grade_override(self):
grade = PersistentSubsectionGrade.create_grade(**self.params)
override = PersistentSubsectionGradeOverride(grade=grade, earned_all_override=0.0, earned_graded_override=0.0)
override.save()
grade = PersistentSubsectionGrade.update_or_create_grade(**self.params)
self.assertEqual(grade.earned_all, 0.0)
self.assertEqual(grade.earned_graded, 0.0)
def _assert_tracker_emitted_event(self, tracker_mock, grade):
"""
Helper function to ensure that the mocked event tracker
......
import ddt
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from lms.djangoapps.grades.services import GradesService
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@ddt.ddt
class GradesServiceTests(ModuleStoreTestCase):
"""
Tests for the Grades service
"""
def setUp(self, **kwargs):
super(GradesServiceTests, self).setUp()
self.service = GradesService()
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
self.subsection = ItemFactory.create(parent=self.course, category="subsection", display_name="Subsection")
self.user = UserFactory()
self.grade = PersistentSubsectionGrade.update_or_create_grade(
user_id=self.user.id,
course_id=self.course.id,
usage_key=self.subsection.location,
first_attempted=None,
visible_blocks=[],
earned_all=6.0,
possible_all=6.0,
earned_graded=5.0,
possible_graded=5.0
)
def test_get_subsection_grade(self):
self.assertDictEqual(self.service.get_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
subsection=self.subsection.location
), {
'earned_all': 6.0,
'earned_graded': 5.0
})
@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):
PersistentSubsectionGradeOverride.objects.all().delete() # clear out all previous overrides
self.service.override_subsection_grade(
user_id=self.user.id,
course_key_or_id=self.course.id,
subsection=self.subsection.location,
earned_all=override['earned_all'],
earned_graded=override['earned_graded']
)
grade = PersistentSubsectionGrade.objects.get(
user_id=self.user.id,
course_id=self.course.id,
usage_key=self.subsection.location
)
self.assertEqual(grade.earned_all, expected['earned_all'])
self.assertEqual(grade.earned_graded, expected['earned_graded'])
......@@ -63,18 +63,22 @@ def run():
analytics.write_key = settings.LMS_SEGMENT_KEY
# register any dependency injections that we need to support in edx_proctoring
# right now edx_proctoring is dependent on the openedx.core.djangoapps.credit
# right now edx_proctoring is dependent on the openedx.core.djangoapps.credit and
# lms.djangoapps.grades
if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
# Import these here to avoid circular dependencies of the form:
# edx-platform app --> DRF --> django translation --> edx-platform app
from edx_proctoring.runtime import set_runtime_service
from lms.djangoapps.instructor.services import InstructorService
from openedx.core.djangoapps.credit.services import CreditService
from lms.djangoapps.grades.services import GradesService
set_runtime_service('credit', CreditService())
# register InstructorService (for deleting student attempts and user staff access roles)
set_runtime_service('instructor', InstructorService())
set_runtime_service('grades', GradesService())
# In order to allow modules to use a handler url, we need to
# monkey-patch the x_module library.
# TODO: Remove this code when Runtimes are no longer created by modulestores
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment