Commit 37b8e636 by Nimisha Asthagiri

HiddenContentTransformer to check for hide_after_due

parent eb02f2ad
...@@ -3,6 +3,7 @@ API function for retrieving course blocks data ...@@ -3,6 +3,7 @@ API function for retrieving course blocks data
""" """
from lms.djangoapps.course_blocks.api import get_course_blocks, COURSE_BLOCK_ACCESS_TRANSFORMERS from lms.djangoapps.course_blocks.api import get_course_blocks, COURSE_BLOCK_ACCESS_TRANSFORMERS
from lms.djangoapps.course_blocks.transformers.hidden_content import HiddenContentTransformer
from openedx.core.lib.block_structure.transformers import BlockStructureTransformers from openedx.core.lib.block_structure.transformers import BlockStructureTransformers
from .transformers.blocks_api import BlocksAPITransformer from .transformers.blocks_api import BlocksAPITransformer
...@@ -51,7 +52,7 @@ def get_blocks( ...@@ -51,7 +52,7 @@ def get_blocks(
# create ordered list of transformers, adding BlocksAPITransformer at end. # create ordered list of transformers, adding BlocksAPITransformer at end.
transformers = BlockStructureTransformers() transformers = BlockStructureTransformers()
if user is not None: if user is not None:
transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS + [ProctoredExamTransformer()] transformers += COURSE_BLOCK_ACCESS_TRANSFORMERS + [ProctoredExamTransformer(), HiddenContentTransformer()]
transformers += [ transformers += [
BlocksAPITransformer( BlocksAPITransformer(
block_counts, block_counts,
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
API entry point to the course_blocks app with top-level API entry point to the course_blocks app with top-level
get_course_blocks function. get_course_blocks function.
""" """
from django.core.cache import cache
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.core.lib.block_structure.manager import BlockStructureManager
from openedx.core.lib.block_structure.transformers import BlockStructureTransformers from openedx.core.lib.block_structure.transformers import BlockStructureTransformers
from xmodule.modulestore.django import modulestore
from .transformers import ( from .transformers import (
library_content, library_content,
......
"""
Visibility Transformer implementation.
"""
from datetime import datetime
from pytz import utc
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
from xmodule.seq_module import SequenceModule
from .utils import collect_merged_boolean_field, collect_merged_date_field
MAXIMUM_DATE = utc.localize(datetime.max)
class HiddenContentTransformer(FilteringTransformerMixin, BlockStructureTransformer):
"""
A transformer that enforces the hide_after_due field on
blocks by removing children blocks from the block structure for
which the user does not have access. The due and hide_after_due
fields on a block is percolated down to its descendants, so that
all blocks enforce the hidden content settings from their ancestors.
For a block with multiple parents, access is denied only if
access is denied from all its parents.
Staff users are exempted from hidden content rules.
"""
VERSION = 1
MERGED_DUE_DATE = 'merged_due_date'
MERGED_HIDE_AFTER_DUE = 'merged_hide_after_due'
@classmethod
def name(cls):
"""
Unique identifier for the transformer's class;
same identifier used in setup.py.
"""
return "hidden_content"
@classmethod
def _get_merged_hide_after_due(cls, block_structure, block_key):
"""
Returns whether the block with the given block_key in the
given block_structure should be visible to staff only per
computed value from ancestry chain.
"""
return block_structure.get_transformer_block_field(
block_key, cls, cls.MERGED_HIDE_AFTER_DUE, False
)
@classmethod
def _get_merged_due_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_DUE_DATE, False
)
@classmethod
def collect(cls, block_structure):
"""
Collects any information that's necessary to execute this
transformer's transform method.
"""
collect_merged_boolean_field(
block_structure,
transformer=cls,
xblock_field_name='hide_after_due',
merged_field_name=cls.MERGED_HIDE_AFTER_DUE,
)
collect_merged_date_field(
block_structure,
transformer=cls,
xblock_field_name='due',
merged_field_name=cls.MERGED_DUE_DATE,
default_date=MAXIMUM_DATE,
func_merge_parents=max,
func_merge_ancestors=min,
)
def transform_block_filters(self, usage_info, block_structure):
# Users with staff access bypass the Visibility check.
if usage_info.has_staff_access:
return [block_structure.create_universal_filter()]
return [
block_structure.create_removal_filter(
lambda block_key: self._is_block_hidden(block_structure, block_key),
),
]
def _is_block_hidden(self, block_structure, block_key):
"""
Returns whether the block with the given block_key should
be hidden, given the current time.
"""
due = self._get_merged_due_date(block_structure, block_key)
hide_after_due = self._get_merged_hide_after_due(block_structure, block_key)
return not SequenceModule.verify_current_content_visibility(due, hide_after_due)
...@@ -5,7 +5,7 @@ from openedx.core.lib.block_structure.transformer import BlockStructureTransform ...@@ -5,7 +5,7 @@ from openedx.core.lib.block_structure.transformer import BlockStructureTransform
from lms.djangoapps.courseware.access_utils import check_start_date from lms.djangoapps.courseware.access_utils import check_start_date
from xmodule.course_metadata_utils import DEFAULT_START_DATE from xmodule.course_metadata_utils import DEFAULT_START_DATE
from .utils import get_field_on_block from .utils import collect_merged_date_field
class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer): class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer):
...@@ -36,7 +36,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer) ...@@ -36,7 +36,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
return "start_date" return "start_date"
@classmethod @classmethod
def get_merged_start_date(cls, block_structure, block_key): def _get_merged_start_date(cls, block_structure, block_key):
""" """
Returns the merged value for the start date for the block with Returns the merged value for the start date for the block with
the given block_key in the given block_structure. the given block_key in the given block_structure.
...@@ -53,35 +53,15 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer) ...@@ -53,35 +53,15 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
""" """
block_structure.request_xblock_fields('days_early_for_beta') block_structure.request_xblock_fields('days_early_for_beta')
for block_key in block_structure.topological_traversal(): collect_merged_date_field(
block_structure,
# compute merged value of start date from all parents transformer=cls,
parents = block_structure.get_parents(block_key) xblock_field_name='start',
min_all_parents_start_date = min( merged_field_name=cls.MERGED_START_DATE,
cls.get_merged_start_date(block_structure, parent_key) default_date=DEFAULT_START_DATE,
for parent_key in parents func_merge_parents=min,
) if parents else None func_merge_ancestors=max,
)
# 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_block_filters(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
# Users with staff access bypass the Start Date check. # Users with staff access bypass the Start Date check.
...@@ -91,7 +71,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer) ...@@ -91,7 +71,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
removal_condition = lambda block_key: not check_start_date( removal_condition = lambda block_key: not check_start_date(
usage_info.user, usage_info.user,
block_structure.get_xblock_field(block_key, 'days_early_for_beta'), block_structure.get_xblock_field(block_key, 'days_early_for_beta'),
self.get_merged_start_date(block_structure, block_key), self._get_merged_start_date(block_structure, block_key),
usage_info.course_key, usage_info.course_key,
) )
return [block_structure.create_removal_filter(removal_condition)] return [block_structure.create_removal_filter(removal_condition)]
...@@ -255,7 +255,7 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) ...@@ -255,7 +255,7 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
self, self,
test_user, test_user,
expected_user_accessible_blocks, expected_user_accessible_blocks,
blocks_with_differing_access, blocks_with_differing_access=None,
transformers=None, transformers=None,
): ):
""" """
...@@ -272,7 +272,8 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) ...@@ -272,7 +272,8 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
blocks_with_differing_access (set(int)): Set of blocks_with_differing_access (set(int)): Set of
blocks (indices) whose access will differ from the blocks (indices) whose access will differ from the
transformers result and the current implementation of transformers result and the current implementation of
has_access. has_access. If not provided, does not compare with
has_access results.
transformers (BlockStructureTransformers): An optional collection transformers (BlockStructureTransformers): An optional collection
of transformers that are to be executed. If not of transformers that are to be executed. If not
...@@ -312,7 +313,6 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) ...@@ -312,7 +313,6 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
# compute access results of the block # compute access results of the block
block_structure_result = xblock_key in block_structure block_structure_result = xblock_key in block_structure
has_access_result = bool(has_access(user, 'load', self.get_block(i), course_key=self.course.id))
# compare with expected value # compare with expected value
self.assertEquals( self.assertEquals(
...@@ -323,23 +323,25 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase) ...@@ -323,23 +323,25 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
) )
) )
# compare with has_access_result if blocks_with_differing_access:
if i in blocks_with_differing_access: # compare with has_access_result
self.assertNotEqual( has_access_result = bool(has_access(user, 'load', self.get_block(i), course_key=self.course.id))
block_structure_result, if i in blocks_with_differing_access:
has_access_result, self.assertNotEqual(
"block structure ({0}) & has_access ({1}) results are equal for block {2} for user {3}".format( block_structure_result,
block_structure_result, has_access_result, i, user.username has_access_result,
"block structure ({0}) & has_access ({1}) results are equal for block {2} for user {3}".format(
block_structure_result, has_access_result, i, user.username
)
) )
) else:
else: self.assertEquals(
self.assertEquals( block_structure_result,
block_structure_result, has_access_result,
has_access_result, "block structure ({0}) & has_access ({1}) results not equal for block {2} for user {3}".format(
"block structure ({0}) & has_access ({1}) results not equal for block {2} for user {3}".format( block_structure_result, has_access_result, i, user.username
block_structure_result, has_access_result, i, user.username )
) )
)
self.client.logout() self.client.logout()
......
"""
Tests for HiddenContentTransformer.
"""
from datetime import timedelta
import ddt
from django.utils.timezone import now
from nose.plugins.attrib import attr
from ..hidden_content import HiddenContentTransformer
from .helpers import BlockParentsMapTestCase, update_block
@attr('shard_3')
@ddt.ddt
class HiddenContentTransformerTestCase(BlockParentsMapTestCase):
"""
VisibilityTransformer Test
"""
TRANSFORMER_CLASS_TO_TEST = HiddenContentTransformer
ALL_BLOCKS = {0, 1, 2, 3, 4, 5, 6}
class DueDateType(object):
"""
Use constant enum types for deterministic ddt test method names (rather than dynamically generated timestamps)
"""
none = 1,
future = 2,
past = 3
TODAY = now()
PAST_DATE = TODAY - timedelta(days=30)
FUTURE_DATE = TODAY + timedelta(days=30)
@classmethod
def due(cls, enum_value):
"""
Returns a start date for the given enum value
"""
if enum_value == cls.future:
return cls.FUTURE_DATE
elif enum_value == cls.past:
return cls.PAST_DATE
else:
return None
# Following test cases are based on BlockParentsMapTestCase.parents_map
@ddt.data(
({}, ALL_BLOCKS),
({0: DueDateType.none}, ALL_BLOCKS),
({0: DueDateType.future}, ALL_BLOCKS),
({1: DueDateType.none}, ALL_BLOCKS),
({1: DueDateType.future}, ALL_BLOCKS),
({4: DueDateType.none}, ALL_BLOCKS),
({4: DueDateType.future}, ALL_BLOCKS),
({0: DueDateType.past}, {}),
({1: DueDateType.past}, ALL_BLOCKS - {1, 3, 4}),
({2: DueDateType.past}, ALL_BLOCKS - {2, 5}),
({4: DueDateType.past}, ALL_BLOCKS - {4}),
({1: DueDateType.past, 2: DueDateType.past}, {0}),
({1: DueDateType.none, 2: DueDateType.past}, ALL_BLOCKS - {2, 5}),
({1: DueDateType.past, 2: DueDateType.none}, ALL_BLOCKS - {1, 3, 4}),
)
@ddt.unpack
def test_hidden_content(
self,
hide_due_values,
expected_visible_blocks,
):
for idx, due_date_type in hide_due_values.iteritems():
block = self.get_block(idx)
block.due = self.DueDateType.due(due_date_type)
block.hide_after_due = True
update_block(block)
self.assert_transform_results(
self.student,
expected_visible_blocks,
blocks_with_differing_access=None,
transformers=self.transformers,
)
...@@ -10,7 +10,103 @@ def get_field_on_block(block, field_name, default_value=None): ...@@ -10,7 +10,103 @@ def get_field_on_block(block, field_name, default_value=None):
returns value from only a single parent chain returns value from only a single parent chain
(e.g., doesn't take a union in DAGs). (e.g., doesn't take a union in DAGs).
""" """
if block.fields[field_name].is_set_on(block): try:
return getattr(block, field_name) if block.fields[field_name].is_set_on(block):
else: return getattr(block, field_name)
return default_value except KeyError:
pass
return default_value
def collect_merged_boolean_field(
block_structure,
transformer,
xblock_field_name,
merged_field_name,
):
"""
Collects a boolean xBlock field of name xblock_field_name
for the given block_structure and transformer. The boolean
value is percolated down the hierarchy of the block_structure
and stored as a value of merged_field_name in the
block_structure.
Assumes that the boolean field is False, by default. So,
the value is ANDed across all parents for blocks with
multiple parents and ORed across all ancestors down a single
hierarchy chain.
"""
for block_key in block_structure.topological_traversal():
# compute merged value of the boolean field from all parents
parents = block_structure.get_parents(block_key)
all_parents_merged_value = all( # pylint: disable=invalid-name
block_structure.get_transformer_block_field(
parent_key, transformer, merged_field_name, False,
)
for parent_key in parents
) if parents else False
# set the merged value for this block
block_structure.set_transformer_block_field(
block_key,
transformer,
merged_field_name,
(
all_parents_merged_value or
get_field_on_block(
block_structure.get_xblock(block_key), xblock_field_name,
False,
)
)
)
def collect_merged_date_field(
block_structure,
transformer,
xblock_field_name,
merged_field_name,
default_date,
func_merge_parents=min,
func_merge_ancestors=max,
):
"""
Collects a date xBlock field of name xblock_field_name
for the given block_structure and transformer. The date
value is percolated down the hierarchy of the block_structure
and stored as a value of merged_field_name in the
block_structure.
"""
for block_key in block_structure.topological_traversal():
parents = block_structure.get_parents(block_key)
block_date = get_field_on_block(block_structure.get_xblock(block_key), xblock_field_name)
if not parents:
# no parents so just use value on block or default
merged_date_value = block_date or default_date
else:
# compute merged value of date from all parents
merged_all_parents_date = func_merge_parents(
block_structure.get_transformer_block_field(
parent_key, transformer, merged_field_name, default_date,
)
for parent_key in parents
)
if not block_date:
# no value on this block so take value from parents
merged_date_value = merged_all_parents_date
else:
# compute merged date of the block and the parent
merged_date_value = func_merge_ancestors(merged_all_parents_date, block_date)
block_structure.set_transformer_block_field(
block_key,
transformer,
merged_field_name,
merged_date_value
)
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Visibility Transformer implementation. Visibility Transformer implementation.
""" """
from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin from openedx.core.lib.block_structure.transformer import BlockStructureTransformer, FilteringTransformerMixin
from .utils import collect_merged_boolean_field
class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer): class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer):
...@@ -30,7 +31,7 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer ...@@ -30,7 +31,7 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
return "visibility" return "visibility"
@classmethod @classmethod
def get_visible_to_staff_only(cls, block_structure, block_key): def _get_visible_to_staff_only(cls, block_structure, block_key):
""" """
Returns whether the block with the given block_key in the Returns whether the block with the given block_key in the
given block_structure should be visible to staff only per given block_structure should be visible to staff only per
...@@ -46,26 +47,12 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer ...@@ -46,26 +47,12 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
Collects any information that's necessary to execute this Collects any information that's necessary to execute this
transformer's transform method. transformer's transform method.
""" """
for block_key in block_structure.topological_traversal(): collect_merged_boolean_field(
block_structure,
# compute merged value of visible_to_staff_only from all parents transformer=cls,
parents = block_structure.get_parents(block_key) xblock_field_name='visible_to_staff_only',
all_parents_visible_to_staff_only = all( # pylint: disable=invalid-name merged_field_name=cls.MERGED_VISIBLE_TO_STAFF_ONLY,
cls.get_visible_to_staff_only(block_structure, parent_key) )
for parent_key in parents
) if parents else False
# set the merged value for this block
block_structure.set_transformer_block_field(
block_key,
cls,
cls.MERGED_VISIBLE_TO_STAFF_ONLY,
# merge visible_to_staff_only from all parents and this block
(
all_parents_visible_to_staff_only or
block_structure.get_xblock(block_key).visible_to_staff_only
)
)
def transform_block_filters(self, usage_info, block_structure): def transform_block_filters(self, usage_info, block_structure):
# Users with staff access bypass the Visibility check. # Users with staff access bypass the Visibility check.
...@@ -74,6 +61,6 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer ...@@ -74,6 +61,6 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
return [ return [
block_structure.create_removal_filter( block_structure.create_removal_filter(
lambda block_key: self.get_visible_to_staff_only(block_structure, block_key), lambda block_key: self._get_visible_to_staff_only(block_structure, block_key),
) )
] ]
...@@ -49,6 +49,7 @@ setup( ...@@ -49,6 +49,7 @@ setup(
"start_date = lms.djangoapps.course_blocks.transformers.start_date:StartDateTransformer", "start_date = lms.djangoapps.course_blocks.transformers.start_date:StartDateTransformer",
"user_partitions = lms.djangoapps.course_blocks.transformers.user_partitions:UserPartitionTransformer", "user_partitions = lms.djangoapps.course_blocks.transformers.user_partitions:UserPartitionTransformer",
"visibility = lms.djangoapps.course_blocks.transformers.visibility:VisibilityTransformer", "visibility = lms.djangoapps.course_blocks.transformers.visibility:VisibilityTransformer",
"hidden_content = lms.djangoapps.course_blocks.transformers.hidden_content:HiddenContentTransformer",
"course_blocks_api = lms.djangoapps.course_api.blocks.transformers.blocks_api:BlocksAPITransformer", "course_blocks_api = lms.djangoapps.course_api.blocks.transformers.blocks_api:BlocksAPITransformer",
"proctored_exam = lms.djangoapps.course_api.blocks.transformers.proctored_exam:ProctoredExamTransformer", "proctored_exam = lms.djangoapps.course_api.blocks.transformers.proctored_exam:ProctoredExamTransformer",
"grades = lms.djangoapps.courseware.transformers.grades:GradesTransformer", "grades = lms.djangoapps.courseware.transformers.grades:GradesTransformer",
......
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