Commit 4c353d93 by Tyler Hallada

Create GradesService to override persistent grades

parent f86f5998
...@@ -411,6 +411,24 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel): ...@@ -411,6 +411,24 @@ class PersistentSubsectionGrade(DeleteGradesMixin, TimeStampedModel):
user_id = params.pop('user_id') user_id = params.pop('user_id')
usage_key = params.pop('usage_key') 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( grade, _ = cls.objects.update_or_create(
user_id=user_id, user_id=user_id,
course_id=usage_key.course_key, course_id=usage_key.course_key,
...@@ -666,3 +684,20 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel): ...@@ -666,3 +684,20 @@ class PersistentCourseGrade(DeleteGradesMixin, TimeStampedModel):
'grading_policy_hash': unicode(grade.grading_policy_hash), '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 ( ...@@ -23,6 +23,7 @@ from lms.djangoapps.grades.models import (
BlockRecordList, BlockRecordList,
PersistentCourseGrade, PersistentCourseGrade,
PersistentSubsectionGrade, PersistentSubsectionGrade,
PersistentSubsectionGradeOverride,
VisibleBlocks VisibleBlocks
) )
from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type from track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
...@@ -306,6 +307,14 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -306,6 +307,14 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
grade = PersistentSubsectionGrade.create_grade(**self.params) grade = PersistentSubsectionGrade.create_grade(**self.params)
self._assert_tracker_emitted_event(tracker_mock, grade) 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): def _assert_tracker_emitted_event(self, tracker_mock, grade):
""" """
Helper function to ensure that the mocked event tracker 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(): ...@@ -63,18 +63,22 @@ def run():
analytics.write_key = settings.LMS_SEGMENT_KEY analytics.write_key = settings.LMS_SEGMENT_KEY
# register any dependency injections that we need to support in edx_proctoring # 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'): if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
# Import these here to avoid circular dependencies of the form: # Import these here to avoid circular dependencies of the form:
# edx-platform app --> DRF --> django translation --> edx-platform app # edx-platform app --> DRF --> django translation --> edx-platform app
from edx_proctoring.runtime import set_runtime_service from edx_proctoring.runtime import set_runtime_service
from lms.djangoapps.instructor.services import InstructorService from lms.djangoapps.instructor.services import InstructorService
from openedx.core.djangoapps.credit.services import CreditService from openedx.core.djangoapps.credit.services import CreditService
from lms.djangoapps.grades.services import GradesService
set_runtime_service('credit', CreditService()) set_runtime_service('credit', CreditService())
# register InstructorService (for deleting student attempts and user staff access roles) # register InstructorService (for deleting student attempts and user staff access roles)
set_runtime_service('instructor', InstructorService()) set_runtime_service('instructor', InstructorService())
set_runtime_service('grades', GradesService())
# In order to allow modules to use a handler url, we need to # In order to allow modules to use a handler url, we need to
# monkey-patch the x_module library. # monkey-patch the x_module library.
# TODO: Remove this code when Runtimes are no longer created by modulestores # 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