Commit bf421108 by Eric Fischer Committed by GitHub

Merge pull request #13295 from edx/efischer/opt_grade

Grades cleanup
parents fb20312e 13687e47
...@@ -40,22 +40,11 @@ def aggregate_scores(scores, section_name="summary", location=None): ...@@ -40,22 +40,11 @@ def aggregate_scores(scores, section_name="summary", location=None):
total_correct = float_sum(score.earned for score in scores) total_correct = float_sum(score.earned for score in scores)
total_possible = float_sum(score.possible for score in scores) total_possible = float_sum(score.possible for score in scores)
#regardless of whether or not it is graded #regardless of whether it is graded
all_total = Score( all_total = Score(total_correct, total_possible, False, section_name, location)
total_correct,
total_possible,
False,
section_name,
location,
)
#selecting only graded things #selecting only graded things
graded_total = Score( graded_total = Score(total_correct_graded, total_possible_graded, True, section_name, location)
total_correct_graded,
total_possible_graded,
True,
section_name,
location,
)
return all_total, graded_total return all_total, graded_total
......
...@@ -12,7 +12,7 @@ import json ...@@ -12,7 +12,7 @@ import json
import logging import logging
from operator import attrgetter from operator import attrgetter
from django.db import models from django.db import models, transaction
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -107,7 +107,8 @@ class VisibleBlocksQuerySet(models.QuerySet): ...@@ -107,7 +107,8 @@ class VisibleBlocksQuerySet(models.QuerySet):
blocks = BlockRecordSet(blocks) blocks = BlockRecordSet(blocks)
try: try:
model = self.create_from_blockrecords(blocks) with transaction.atomic():
model = self.create_from_blockrecords(blocks)
except IntegrityError: except IntegrityError:
# If an integrity error occurs, the VisibleBlocks model we want to # If an integrity error occurs, the VisibleBlocks model we want to
# create already exists. The hash is still the correct value. # create already exists. The hash is still the correct value.
...@@ -172,13 +173,10 @@ class PersistentSubsectionGradeQuerySet(models.QuerySet): ...@@ -172,13 +173,10 @@ class PersistentSubsectionGradeQuerySet(models.QuerySet):
kwargs['course_id'] = kwargs['usage_key'].course_key kwargs['course_id'] = kwargs['usage_key'].course_key
visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(blocks=visible_blocks) visible_blocks_hash = VisibleBlocks.objects.hash_from_blockrecords(blocks=visible_blocks)
grade = self.model( return super(PersistentSubsectionGradeQuerySet, self).create(
visible_blocks_id=visible_blocks_hash, visible_blocks_id=visible_blocks_hash,
**kwargs **kwargs
) )
grade.full_clean()
grade.save()
return grade
class PersistentSubsectionGrade(TimeStampedModel): class PersistentSubsectionGrade(TimeStampedModel):
...@@ -244,12 +242,13 @@ class PersistentSubsectionGrade(TimeStampedModel): ...@@ -244,12 +242,13 @@ class PersistentSubsectionGrade(TimeStampedModel):
user_id = kwargs.pop('user_id') user_id = kwargs.pop('user_id')
usage_key = kwargs.pop('usage_key') usage_key = kwargs.pop('usage_key')
try: try:
grade, is_created = cls.objects.get_or_create( with transaction.atomic():
user_id=user_id, grade, is_created = cls.objects.get_or_create(
course_id=usage_key.course_key, user_id=user_id,
usage_key=usage_key, course_id=usage_key.course_key,
defaults=kwargs, usage_key=usage_key,
) defaults=kwargs,
)
except IntegrityError: except IntegrityError:
cls.update_grade(user_id=user_id, usage_key=usage_key, **kwargs) cls.update_grade(user_id=user_id, usage_key=usage_key, **kwargs)
else: else:
......
...@@ -44,11 +44,15 @@ class SubsectionGrade(object): ...@@ -44,11 +44,15 @@ class SubsectionGrade(object):
""" """
Compute the grade of this subsection for the given student and course. Compute the grade of this subsection for the given student and course.
""" """
for descendant_key in course_structure.post_order_traversal( try:
filter_func=possibly_scored, for descendant_key in course_structure.post_order_traversal(
start_node=self.location, filter_func=possibly_scored,
): start_node=self.location,
self._compute_block_score(student, descendant_key, course_structure, scores_client, submissions_scores) ):
self._compute_block_score(student, descendant_key, course_structure, scores_client, submissions_scores)
finally:
# self.scores may hold outdated data, force it to refresh on next access
lazy.invalidate(self, 'scores')
self.all_total, self.graded_total = graders.aggregate_scores(self.scores, self.display_name, self.location) self.all_total, self.graded_total = graders.aggregate_scores(self.scores, self.display_name, self.location)
......
...@@ -8,7 +8,7 @@ from hashlib import sha1 ...@@ -8,7 +8,7 @@ from hashlib import sha1
import json import json
from mock import patch from mock import patch
from django.core.exceptions import ValidationError from django.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
...@@ -156,7 +156,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -156,7 +156,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
usage_key=self.params["usage_key"], usage_key=self.params["usage_key"],
) )
self.assertEqual(created_grade, read_grade) self.assertEqual(created_grade, read_grade)
with self.assertRaises(ValidationError): with self.assertRaises(IntegrityError):
created_grade = PersistentSubsectionGrade.objects.create(**self.params) created_grade = PersistentSubsectionGrade.objects.create(**self.params)
def test_create_bad_params(self): def test_create_bad_params(self):
...@@ -164,7 +164,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase): ...@@ -164,7 +164,7 @@ class PersistentSubsectionGradeTest(GradesModelTestCase):
Confirms create will fail if params are missing. Confirms create will fail if params are missing.
""" """
del self.params["earned_graded"] del self.params["earned_graded"]
with self.assertRaises(ValidationError): with self.assertRaises(IntegrityError):
PersistentSubsectionGrade.objects.create(**self.params) PersistentSubsectionGrade.objects.create(**self.params)
def test_course_version_is_optional(self): def test_course_version_is_optional(self):
......
...@@ -17,6 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory ...@@ -17,6 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from ..models import PersistentSubsectionGrade from ..models import PersistentSubsectionGrade
from ..new.course_grade import CourseGradeFactory from ..new.course_grade import CourseGradeFactory
from ..new.subsection_grade import SubsectionGrade, SubsectionGradeFactory from ..new.subsection_grade import SubsectionGrade, SubsectionGradeFactory
from lms.djangoapps.grades.tests.utils import mock_get_score
class GradeTestBase(SharedModuleStoreTestCase): class GradeTestBase(SharedModuleStoreTestCase):
...@@ -128,7 +129,7 @@ class SubsectionGradeFactoryTest(GradeTestBase): ...@@ -128,7 +129,7 @@ class SubsectionGradeFactoryTest(GradeTestBase):
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade', 'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade',
wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access
) as mock_get_saved_grade: ) as mock_get_saved_grade:
with self.assertNumQueries(17): with self.assertNumQueries(19):
grade_a = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course) grade_a = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
self.assertTrue(mock_get_saved_grade.called) self.assertTrue(mock_get_saved_grade.called)
self.assertTrue(mock_save_grades.called) self.assertTrue(mock_save_grades.called)
...@@ -180,11 +181,11 @@ class SubsectionGradeTest(GradeTestBase): ...@@ -180,11 +181,11 @@ class SubsectionGradeTest(GradeTestBase):
Assuming the underlying score reporting methods work, test that the score is calculated properly. Assuming the underlying score reporting methods work, test that the score is calculated properly.
""" """
grade = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course) grade = self.subsection_grade_factory.create(self.sequence, self.course_structure, self.course)
with patch('lms.djangoapps.grades.new.subsection_grade.get_score', return_value=(0, 1)): with mock_get_score(1, 2):
# The final 2 parameters are only passed through to our mocked-out get_score method # The final 2 parameters are only passed through to our mocked-out get_score method
grade.compute(self.request.user, self.course_structure, None, None) grade.compute(self.request.user, self.course_structure, None, None)
self.assertEqual(grade.all_total.earned, 0) self.assertEqual(grade.all_total.earned, 1)
self.assertEqual(grade.all_total.possible, 1) self.assertEqual(grade.all_total.possible, 2)
def test_save_and_load(self): def test_save_and_load(self):
""" """
......
...@@ -203,7 +203,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -203,7 +203,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
def test_subsection_grade_updated_on_signal(self, default_store): def test_subsection_grade_updated_on_signal(self, default_store):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
with check_mongo_calls(2) and self.assertNumQueries(13): with check_mongo_calls(2) and self.assertNumQueries(15):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
...@@ -212,7 +212,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -212,7 +212,7 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
self.set_up_course() self.set_up_course()
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3')
with check_mongo_calls(2) and self.assertNumQueries(13): with check_mongo_calls(2) and self.assertNumQueries(15):
recalculate_subsection_grade_handler(None, **self.score_changed_kwargs) recalculate_subsection_grade_handler(None, **self.score_changed_kwargs)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
...@@ -238,8 +238,8 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase): ...@@ -238,8 +238,8 @@ class ScoreChangedUpdatesSubsectionGradeTest(ModuleStoreTestCase):
SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs) SCORE_CHANGED.send(sender=None, **self.score_changed_kwargs)
@ddt.data( @ddt.data(
('points_possible', 2, 13), ('points_possible', 2, 15),
('points_earned', 2, 13), ('points_earned', 2, 15),
('user', 0, 0), ('user', 0, 0),
('course_id', 0, 0), ('course_id', 0, 0),
('usage_id', 0, 0), ('usage_id', 0, 0),
......
...@@ -13,3 +13,13 @@ def mock_passing_grade(grade_pass='Pass', percent=0.75): ...@@ -13,3 +13,13 @@ def mock_passing_grade(grade_pass='Pass', percent=0.75):
with patch('lms.djangoapps.grades.course_grades.summary') as mock_grade: with patch('lms.djangoapps.grades.course_grades.summary') as mock_grade:
mock_grade.return_value = {'grade': grade_pass, 'percent': percent} mock_grade.return_value = {'grade': grade_pass, 'percent': percent}
yield yield
@contextmanager
def mock_get_score(earned=0, possible=1):
"""
Mocks the get_score function to return a valid grade.
"""
with patch('lms.djangoapps.grades.new.subsection_grade.get_score') as mock_score:
mock_score.return_value = (earned, possible)
yield mock_score
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