Transformer: StartDateTransformer
This commit is contained in:
101
lms/djangoapps/course_blocks/transformers/start_date.py
Normal file
101
lms/djangoapps/course_blocks/transformers/start_date.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Start Date Transformer implementation.
|
||||
"""
|
||||
from openedx.core.lib.block_cache.transformer import BlockStructureTransformer
|
||||
from lms.djangoapps.courseware.access_utils import check_start_date
|
||||
from xmodule.course_metadata_utils import DEFAULT_START_DATE
|
||||
|
||||
from .utils import get_field_on_block
|
||||
|
||||
|
||||
class StartDateTransformer(BlockStructureTransformer):
|
||||
"""
|
||||
A transformer that enforces the 'start' and 'days_early_for_beta'
|
||||
fields on blocks by removing blocks from the block structure for
|
||||
which the user does not have access. The 'start' field on a
|
||||
block is percolated down to its descendants, so that all blocks
|
||||
enforce the 'start' field from their ancestors. The assumed
|
||||
'start' value for a block is then the maximum of its parent and its
|
||||
own.
|
||||
|
||||
For a block with multiple parents, the assumed parent start date
|
||||
value is a computed minimum of the start dates of all its parents.
|
||||
So as long as one parent chain allows access, the block has access.
|
||||
|
||||
Staff users are exempted from visibility rules.
|
||||
"""
|
||||
VERSION = 1
|
||||
MERGED_START_DATE = 'merged_start_date'
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
"""
|
||||
Unique identifier for the transformer's class;
|
||||
same identifier used in setup.py.
|
||||
"""
|
||||
return "start_date"
|
||||
|
||||
@classmethod
|
||||
def get_merged_start_date(cls, block_structure, block_key):
|
||||
"""
|
||||
Returns the merged value for the start date for the block with
|
||||
the given block_key in the given block_structure.
|
||||
"""
|
||||
return block_structure.get_transformer_block_field(
|
||||
block_key, cls, cls.MERGED_START_DATE, False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def collect(cls, block_structure):
|
||||
"""
|
||||
Collects any information that's necessary to execute this
|
||||
transformer's transform method.
|
||||
"""
|
||||
block_structure.request_xblock_fields('days_early_for_beta')
|
||||
|
||||
for block_key in block_structure.topological_traversal():
|
||||
|
||||
# compute merged value of start date from all parents
|
||||
parents = block_structure.get_parents(block_key)
|
||||
min_all_parents_start_date = min(
|
||||
cls.get_merged_start_date(block_structure, parent_key)
|
||||
for parent_key in parents
|
||||
) if parents else None
|
||||
|
||||
# set the merged value for this block
|
||||
block_start = get_field_on_block(block_structure.get_xblock(block_key), 'start')
|
||||
if min_all_parents_start_date is None:
|
||||
# no parents so just use value on block or default
|
||||
merged_start_value = block_start or DEFAULT_START_DATE
|
||||
|
||||
elif not block_start:
|
||||
# no value on this block so take value from parents
|
||||
merged_start_value = min_all_parents_start_date
|
||||
|
||||
else:
|
||||
# max of merged-start-from-all-parents and this block
|
||||
merged_start_value = max(min_all_parents_start_date, block_start)
|
||||
|
||||
block_structure.set_transformer_block_field(
|
||||
block_key,
|
||||
cls,
|
||||
cls.MERGED_START_DATE,
|
||||
merged_start_value
|
||||
)
|
||||
|
||||
def transform(self, usage_info, block_structure):
|
||||
"""
|
||||
Mutates block_structure based on the given usage_info.
|
||||
"""
|
||||
# Users with staff access bypass the Start Date check.
|
||||
if usage_info.has_staff_access:
|
||||
return
|
||||
|
||||
block_structure.remove_block_if(
|
||||
lambda block_key: not check_start_date(
|
||||
usage_info.user,
|
||||
block_structure.get_xblock_field(block_key, 'days_early_for_beta'),
|
||||
self.get_merged_start_date(block_structure, block_key),
|
||||
usage_info.course_key,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Tests for StartDateTransformer.
|
||||
"""
|
||||
import ddt
|
||||
from datetime import timedelta
|
||||
from django.utils.timezone import now
|
||||
from mock import patch
|
||||
|
||||
from courseware.tests.factories import BetaTesterFactory
|
||||
from ..start_date import StartDateTransformer, DEFAULT_START_DATE
|
||||
from .test_helpers import BlockParentsMapTestCase, update_block
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class StartDateTransformerTestCase(BlockParentsMapTestCase):
|
||||
"""
|
||||
StartDateTransformer Test
|
||||
"""
|
||||
STUDENT = 1
|
||||
BETA_USER = 2
|
||||
|
||||
class StartDateType(object):
|
||||
"""
|
||||
Use constant enum types for deterministic ddt test method names (rather than dynamically generated timestamps)
|
||||
"""
|
||||
released = 1,
|
||||
future = 2,
|
||||
default = 3
|
||||
|
||||
TODAY = now()
|
||||
LAST_MONTH = TODAY - timedelta(days=30)
|
||||
NEXT_MONTH = TODAY + timedelta(days=30)
|
||||
|
||||
@classmethod
|
||||
def start(cls, enum_value):
|
||||
"""
|
||||
Returns a start date for the given enum value
|
||||
"""
|
||||
if enum_value == cls.released:
|
||||
return cls.LAST_MONTH
|
||||
elif enum_value == cls.future:
|
||||
return cls.NEXT_MONTH
|
||||
else:
|
||||
return DEFAULT_START_DATE
|
||||
|
||||
def setUp(self, **kwargs):
|
||||
super(StartDateTransformerTestCase, self).setUp(**kwargs)
|
||||
self.beta_user = BetaTesterFactory(course_key=self.course.id, username='beta_tester', password=self.password)
|
||||
course = self.get_block(0)
|
||||
course.days_early_for_beta = 33
|
||||
update_block(course)
|
||||
|
||||
# Following test cases are based on BlockParentsMapTestCase.parents_map:
|
||||
# 0
|
||||
# / \
|
||||
# 1 2
|
||||
# / \ / \
|
||||
# 3 4 / 5
|
||||
# \ /
|
||||
# 6
|
||||
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
|
||||
@ddt.data(
|
||||
(STUDENT, {}, {}, {}),
|
||||
(STUDENT, {0: StartDateType.default}, {}, {}),
|
||||
(STUDENT, {0: StartDateType.future}, {}, {}),
|
||||
(STUDENT, {0: StartDateType.released}, {0, 1, 2, 3, 4, 5, 6}, {}),
|
||||
|
||||
# has_access checks on block directly and doesn't follow negative access set on parent/ancestor (i.e., 0)
|
||||
(STUDENT, {1: StartDateType.released}, {}, {1, 3, 4, 6}),
|
||||
(STUDENT, {2: StartDateType.released}, {}, {2, 5, 6}),
|
||||
(STUDENT, {1: StartDateType.released, 2: StartDateType.released}, {}, {1, 2, 3, 4, 5, 6}),
|
||||
|
||||
# DAG conflicts: has_access relies on field inheritance so it takes only the value from the first parent-chain
|
||||
(STUDENT, {0: StartDateType.released, 4: StartDateType.future}, {0, 1, 2, 3, 5, 6}, {6}),
|
||||
(
|
||||
STUDENT,
|
||||
{0: StartDateType.released, 2: StartDateType.released, 4: StartDateType.future},
|
||||
{0, 1, 2, 3, 5, 6},
|
||||
{6},
|
||||
),
|
||||
(STUDENT, {0: StartDateType.released, 2: StartDateType.future, 4: StartDateType.released}, {0, 1, 3, 4, 6}, {}),
|
||||
|
||||
# beta user cases
|
||||
(BETA_USER, {}, {}, {}),
|
||||
(BETA_USER, {0: StartDateType.default}, {}, {}),
|
||||
(BETA_USER, {0: StartDateType.future}, {0, 1, 2, 3, 4, 5, 6}, {}),
|
||||
(BETA_USER, {0: StartDateType.released}, {0, 1, 2, 3, 4, 5, 6}, {}),
|
||||
|
||||
(
|
||||
BETA_USER,
|
||||
{0: StartDateType.released, 2: StartDateType.default, 5: StartDateType.future},
|
||||
{0, 1, 3, 4, 6},
|
||||
{5},
|
||||
),
|
||||
(BETA_USER, {1: StartDateType.released, 2: StartDateType.default}, {}, {1, 3, 4, 6}),
|
||||
(BETA_USER, {0: StartDateType.released, 4: StartDateType.future}, {0, 1, 2, 3, 4, 5, 6}, {}),
|
||||
(BETA_USER, {0: StartDateType.released, 4: StartDateType.default}, {0, 1, 2, 3, 5, 6}, {6}),
|
||||
)
|
||||
@ddt.unpack
|
||||
# pylint: disable=invalid-name
|
||||
def test_block_start_date(
|
||||
self,
|
||||
user_type,
|
||||
start_date_type_values,
|
||||
expected_student_visible_blocks,
|
||||
blocks_with_differing_student_access
|
||||
):
|
||||
for idx, start_date_type in start_date_type_values.iteritems():
|
||||
block = self.get_block(idx)
|
||||
block.start = self.StartDateType.start(start_date_type)
|
||||
update_block(block)
|
||||
|
||||
self.assert_transform_results(
|
||||
self.beta_user if user_type == self.BETA_USER else self.student,
|
||||
expected_student_visible_blocks,
|
||||
blocks_with_differing_student_access,
|
||||
[StartDateTransformer()],
|
||||
)
|
||||
Reference in New Issue
Block a user