""" Milestones Transformer """ import logging from django.conf import settings from edx_proctoring.api import get_attempt_status_summary from edx_proctoring.exceptions import ProctoredExamNotFoundException from openedx.core.djangoapps.content.block_structure.transformer import ( BlockStructureTransformer, ) from student.models import EntranceExamConfiguration from util import milestones_helpers log = logging.getLogger(__name__) class MilestonesAndSpecialExamsTransformer(BlockStructureTransformer): """ A transformer that handles both milestones and special (timed) exams. It excludes all blocks with unfulfilled milestones from the student view. An entrance exam is considered a milestone, and is not considered a "special exam". It also includes or excludes all special (timed) exams (timed, proctored, practice proctored) in/from the student view, based on the value of `include_special_exams`. """ WRITE_VERSION = 1 READ_VERSION = 1 @classmethod def name(cls): return "milestones" def __init__(self, include_special_exams=True): self.include_special_exams = include_special_exams @classmethod def collect(cls, block_structure): """ Computes any information for each XBlock that's necessary to execute this transformer's transform method. Arguments: block_structure (BlockStructureCollectedData) """ block_structure.request_xblock_fields('is_proctored_enabled') block_structure.request_xblock_fields('is_practice_exam') block_structure.request_xblock_fields('is_timed_exam') block_structure.request_xblock_fields('entrance_exam_id') def transform(self, usage_info, block_structure): """ Modify block structure according to the behavior of milestones and special exams. """ required_content = self.get_required_content(usage_info, block_structure) def user_gated_from_block(block_key): """ Checks whether the user is gated from accessing this block, first via special exams, then via a general milestones check. """ if usage_info.has_staff_access: return False elif self.has_pending_milestones_for_user(block_key, usage_info): return True elif self.gated_by_required_content(block_key, block_structure, required_content): return True elif (settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (self.is_special_exam(block_key, block_structure) and not self.include_special_exams)): return True return False for block_key in block_structure.topological_traversal(): if user_gated_from_block(block_key): block_structure.remove_block(block_key, False) elif self.is_special_exam(block_key, block_structure): self.add_special_exam_info(block_key, block_structure, usage_info) @staticmethod def is_special_exam(block_key, block_structure): """ Test whether the block is a special exam. """ return ( block_structure.get_xblock_field(block_key, 'is_proctored_enabled') or block_structure.get_xblock_field(block_key, 'is_practice_exam') or block_structure.get_xblock_field(block_key, 'is_timed_exam') ) @staticmethod def has_pending_milestones_for_user(block_key, usage_info): """ Test whether the current user has any unfulfilled milestones preventing them from accessing this block. """ return bool(milestones_helpers.get_course_content_milestones( unicode(block_key.course_key), unicode(block_key), 'requires', usage_info.user.id )) # TODO: As part of a cleanup effort, this transformer should be split into # MilestonesTransformer and SpecialExamsTransformer, which are completely independent. def add_special_exam_info(self, block_key, block_structure, usage_info): """ For special exams, add the special exam information to the course blocks. """ special_exam_attempt_context = None try: # Calls into edx_proctoring subsystem to get relevant special exam information. # This will return None, if (user, course_id, content_id) is not applicable. special_exam_attempt_context = get_attempt_status_summary( usage_info.user.id, unicode(block_key.course_key), unicode(block_key) ) except ProctoredExamNotFoundException as ex: log.exception(ex) if special_exam_attempt_context: # This user has special exam context for this block so add it. block_structure.set_transformer_block_field( block_key, self, 'special_exam_info', special_exam_attempt_context, ) @staticmethod def get_required_content(usage_info, block_structure): """ Get the required content for the course. This takes into account if the user can skip the entrance exam. """ course_key = block_structure.root_block_usage_key.course_key user_can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(usage_info.user, course_key) required_content = milestones_helpers.get_required_content(course_key, usage_info.user) if not required_content: return required_content if user_can_skip_entrance_exam: # remove the entrance exam from required content entrance_exam_id = block_structure.get_xblock_field(block_structure.root_block_usage_key, 'entrance_exam_id') required_content = [content for content in required_content if not content == entrance_exam_id] return required_content @staticmethod def gated_by_required_content(block_key, block_structure, required_content): """ Returns True if the current block associated with the block_key should be gated by the given required_content. Returns False otherwise. """ if not required_content: return False if block_key.block_type == 'chapter' and unicode(block_key) not in required_content: return True return False