diff --git a/lms/djangoapps/grades/tests/test_transformer.py b/lms/djangoapps/grades/tests/test_transformer.py index 4ed2976ea6..ee939aa5af 100644 --- a/lms/djangoapps/grades/tests/test_transformer.py +++ b/lms/djangoapps/grades/tests/test_transformer.py @@ -7,9 +7,11 @@ import pytz import random import ddt +from copy import deepcopy from student.tests.factories import UserFactory from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import check_mongo_calls @@ -39,6 +41,27 @@ class GradesTransformerTestCase(CourseStructureTestCase): self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password) self.client.login(username=self.student.username, password=password) + def _update_course_grading_policy(self, course, grading_policy): + """ + Helper to update a course's grading policy in the modulestore. + """ + course.set_grading_policy(grading_policy) + modulestore().update_item(course, self.user.id) + + def _validate_grading_policy_hash(self, course_location, grading_policy_hash): + """ + Helper to retrieve the course at the given course_location and + assert that its hashed grading policy (from the grades transformer) + is as expected. + """ + block_structure = get_course_blocks(self.student, course_location, self.transformers) + self.assert_collected_transformer_block_fields( + block_structure, + course_location, + self.TRANSFORMER_CLASS_TO_TEST, + grading_policy_hash=grading_policy_hash, + ) + def assert_collected_xblock_fields(self, block_structure, usage_key, **expectations): """ Given a block structure, a block usage key, and a list of keyword @@ -330,6 +353,38 @@ class GradesTransformerTestCase(CourseStructureTestCase): block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers) self.assertIsNotNone(block_structure.get_xblock_field(blocks[u'course'].location, u'course_version')) + def test_grading_policy_collected(self): + # the calculated hash of the original and updated grading policies of the test course + original_grading_policy_hash = u'ChVp0lHGQGCevD0t4njna/C44zQ=' + updated_grading_policy_hash = u'TsbX04qWOy1WRnC0NHy+94upPd4=' + + blocks = self.build_course_with_problems() + course_block = blocks[u'course'] + self._validate_grading_policy_hash( + course_block.location, + original_grading_policy_hash + ) + + # make sure the hash changes when the course grading policy is edited + grading_policy_with_updates = course_block.grading_policy + original_grading_policy = deepcopy(grading_policy_with_updates) + for section in grading_policy_with_updates['GRADER']: + self.assertNotEqual(section['weight'], 0.25) + section['weight'] = 0.25 + + self._update_course_grading_policy(course_block, grading_policy_with_updates) + self._validate_grading_policy_hash( + course_block.location, + updated_grading_policy_hash + ) + + # reset the grading policy and ensure the hash matches the original + self._update_course_grading_policy(course_block, original_grading_policy) + self._validate_grading_policy_hash( + course_block.location, + original_grading_policy_hash + ) + class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModuleStoreTestCase): """ diff --git a/lms/djangoapps/grades/transformer.py b/lms/djangoapps/grades/transformer.py index 481c2e700c..40442502ee 100644 --- a/lms/djangoapps/grades/transformer.py +++ b/lms/djangoapps/grades/transformer.py @@ -1,9 +1,12 @@ """ Grades Transformer """ +from base64 import b64encode from django.test.client import RequestFactory from functools import reduce as functools_reduce +from hashlib import sha1 from logging import getLogger +import json from courseware.model_data import FieldDataCache from courseware.module_render import get_module_for_descriptor @@ -64,6 +67,7 @@ class GradesTransformer(BlockStructureTransformer): filter_by=lambda block_key: block_key.block_type == 'sequential', ) cls._collect_explicit_graded(block_structure) + cls._collect_grading_policy_hash(block_structure) def transform(self, block_structure, usage_context): """ @@ -128,6 +132,35 @@ class GradesTransformer(BlockStructureTransformer): if max_score is None: log.warning("GradesTransformer: max_score is None for {}".format(module.location)) + @classmethod + def _collect_grading_policy_hash(cls, block_structure): + """ + Collect a hash of the course's grading policy, storing it as a + `transformer_block_field` associated with the `GradesTransformer`. + """ + def _hash_grading_policy(policy): + """ + Creates a hash from the course grading policy. + The keys are sorted in order to make the hash + agnostic to the ordering of the policy coming in. + """ + ordered_policy = json.dumps( + policy, + separators=(',', ':'), # Remove spaces from separators for more compact representation + sort_keys=True, + ) + return b64encode(sha1(ordered_policy).digest()) + + course_location = block_structure.root_block_usage_key + course_block = block_structure.get_xblock(course_location) + grading_policy = course_block.grading_policy + block_structure.set_transformer_block_field( + course_block.location, + cls, + "grading_policy_hash", + _hash_grading_policy(grading_policy) + ) + @staticmethod def _iter_scorable_xmodules(block_structure): """