Commit dcdbd53e by Nimisha Asthagiri

Transformer: StartDateTransformer

parent 3f6baa99
"""
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,
)
)
"""
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()],
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment