""" Grades Transformer """ import json from base64 import b64encode from functools import reduce as functools_reduce from hashlib import sha1 from logging import getLogger from lms.djangoapps.course_blocks.transformers.utils import collect_unioned_set_field, get_field_on_block from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer log = getLogger(__name__) class GradesTransformer(BlockStructureTransformer): """ The GradesTransformer collects grading information and stores it on the block structure. No runtime transformations are performed. The following values are stored as xblock_fields on their respective blocks in the block structure: due: (datetime) when the problem is due. format: (string) what type of problem it is graded: (boolean) has_score: (boolean) weight: (numeric) show_correctness: (string) when to show grades (one of 'always', 'past_due', 'never') Additionally, the following value is calculated and stored as a transformer_block_field for each block: max_score: (numeric) """ WRITE_VERSION = 4 READ_VERSION = 4 FIELDS_TO_COLLECT = [ u'due', u'format', u'graded', u'has_score', u'weight', u'course_version', u'subtree_edited_on', u'show_correctness', ] EXPLICIT_GRADED_FIELD_NAME = 'explicit_graded' @classmethod def name(cls): """ Unique identifier for the transformer's class; same identifier used in setup.py. """ return u'grades' @classmethod def collect(cls, block_structure): """ Collects any information that's necessary to execute this transformer's transform method. """ block_structure.request_xblock_fields(*cls.FIELDS_TO_COLLECT) cls._collect_max_scores(block_structure) collect_unioned_set_field( block_structure=block_structure, transformer=cls, merged_field_name='subsections', 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): """ Perform no transformations. """ pass @classmethod def grading_policy_hash(cls, course): """ Returns the grading policy hash for the given course. """ ordered_policy = json.dumps( course.grading_policy, separators=(',', ':'), # Remove spaces from separators for more compact representation sort_keys=True, ) return b64encode(sha1(ordered_policy).digest()) @classmethod def _collect_explicit_graded(cls, block_structure): """ Collect the 'explicit_graded' field for every block. """ def _set_field(block_key, field_value): """ Sets the explicit graded field to the given value for the given block. """ block_structure.set_transformer_block_field(block_key, cls, cls.EXPLICIT_GRADED_FIELD_NAME, field_value) def _get_field(block_key): """ Gets the explicit graded field to the given value for the given block. """ return block_structure.get_transformer_block_field(block_key, cls, cls.EXPLICIT_GRADED_FIELD_NAME) block_types_to_ignore = {'course', 'chapter', 'sequential'} for block_key in block_structure.topological_traversal(): if block_key.block_type in block_types_to_ignore: _set_field(block_key, None) else: explicit_field_on_block = get_field_on_block(block_structure.get_xblock(block_key), 'graded') if explicit_field_on_block is not None: _set_field(block_key, explicit_field_on_block) else: values_from_parents = [ _get_field(parent) for parent in block_structure.get_parents(block_key) if parent.block_type not in block_types_to_ignore ] non_null_values_from_parents = [value for value in values_from_parents if not None] explicit_from_parents = functools_reduce(lambda x, y: x or y, non_null_values_from_parents, None) _set_field(block_key, explicit_from_parents) @classmethod def _collect_max_scores(cls, block_structure): """ Collect the `max_score` for every block in the provided `block_structure`. """ for block_locator in block_structure.post_order_traversal(): block = block_structure.get_xblock(block_locator) if getattr(block, 'has_score', False): cls._collect_max_score(block_structure, block) @classmethod def _collect_max_score(cls, block_structure, module): """ Collect the `max_score` from the given module, storing it as a `transformer_block_field` associated with the `GradesTransformer`. """ max_score = module.max_score() block_structure.set_transformer_block_field(module.location, cls, 'max_score', max_score) 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`. """ course_location = block_structure.root_block_usage_key course_block = block_structure.get_xblock(course_location) block_structure.set_transformer_block_field( course_block.location, cls, "grading_policy_hash", cls.grading_policy_hash(course_block), ) @staticmethod def _iter_scorable_xmodules(block_structure): """ Loop through all the blocks locators in the block structure, and retrieve the module (XModule or XBlock) associated with that locator. For implementation reasons, we need to pull the max_score from the XModule, even though the data is not user specific. Here we bind the data to a SystemUser. """ for block_locator in block_structure.post_order_traversal(): block = block_structure.get_xblock(block_locator) if getattr(block, 'has_score', False): yield block