175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
"""
|
|
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 common.djangoapps.student.models import EntranceExamConfiguration
|
|
from common.djangoapps.util import milestones_helpers
|
|
from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class MilestonesAndSpecialExamsTransformer(BlockStructureTransformer):
|
|
"""
|
|
A transformer that handles both milestones and special (timed) exams.
|
|
|
|
It includes or excludes all unfulfilled milestones from the student view based on the value of `include_gated_sections`. # lint-amnesty, pylint: disable=line-too-long
|
|
|
|
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, include_gated_sections=True):
|
|
self.include_special_exams = include_special_exams
|
|
self.include_gated_sections = include_gated_sections
|
|
|
|
@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.gated_by_required_content(block_key, block_structure, required_content):
|
|
return True
|
|
elif not self.include_gated_sections and self.has_pending_milestones_for_user(block_key, usage_info):
|
|
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(
|
|
str(block_key.course_key),
|
|
str(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,
|
|
str(block_key.course_key),
|
|
str(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 = False
|
|
if usage_info.user.is_authenticated:
|
|
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') # lint-amnesty, pylint: disable=line-too-long
|
|
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): # lint-amnesty, pylint: disable=unused-argument
|
|
"""
|
|
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 str(block_key) not in required_content:
|
|
return True
|
|
|
|
return False
|