Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
9d747fa6
Commit
9d747fa6
authored
Jul 20, 2016
by
Nimisha Asthagiri
Committed by
GitHub
Jul 20, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #12975 from edx/tnl/hide_content
LMS Hide Subsection Content
parents
3302b26e
37b8e636
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
562 additions
and
170 deletions
+562
-170
common/lib/xmodule/xmodule/seq_module.py
+139
-71
common/lib/xmodule/xmodule/tests/test_sequence.py
+64
-16
lms/djangoapps/course_api/blocks/api.py
+2
-1
lms/djangoapps/course_blocks/api.py
+0
-3
lms/djangoapps/course_blocks/transformers/hidden_content.py
+102
-0
lms/djangoapps/course_blocks/transformers/start_date.py
+12
-32
lms/djangoapps/course_blocks/transformers/tests/helpers.py
+20
-18
lms/djangoapps/course_blocks/transformers/tests/test_hidden_content.py
+83
-0
lms/djangoapps/course_blocks/transformers/utils.py
+100
-4
lms/djangoapps/course_blocks/transformers/visibility.py
+9
-22
lms/djangoapps/courseware/views/index.py
+2
-1
lms/templates/hidden_content.html
+26
-0
lms/templates/seq_module.html
+2
-2
setup.py
+1
-0
No files found.
common/lib/xmodule/xmodule/seq_module.py
View file @
9d747fa6
...
...
@@ -4,6 +4,8 @@ xModule implementation of a learning sequence
# pylint: disable=abstract-method
import
collections
from
datetime
import
datetime
from
django.utils.timezone
import
UTC
import
json
import
logging
from
pkg_resources
import
resource_string
...
...
@@ -38,13 +40,22 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
position
=
Integer
(
help
=
"Last tab viewed in this sequence"
,
scope
=
Scope
.
user_state
)
due
=
Date
(
display_name
=
_
(
"Due Date"
),
help
=
_
(
"Enter the date by which problems are due."
),
scope
=
Scope
.
settings
,
)
# Entrance Exam flag -- see cms/contentstore/views/entrance_exam.py for usage
hide_after_due
=
Boolean
(
display_name
=
_
(
"Hide sequence content After Due Date"
),
help
=
_
(
"If set, the sequence content is hidden for non-staff users after the due date has passed."
),
default
=
False
,
scope
=
Scope
.
settings
,
)
is_entrance_exam
=
Boolean
(
display_name
=
_
(
"Is Entrance Exam"
),
help
=
_
(
...
...
@@ -97,16 +108,6 @@ class ProctoringFields(object):
scope
=
Scope
.
settings
,
)
hide_after_due
=
Boolean
(
display_name
=
_
(
"Hide Exam Results After Due Date"
),
help
=
_
(
"This setting overrides the default behavior of showing exam results after the due date has passed."
" Currently only supported for timed exams."
),
default
=
False
,
scope
=
Scope
.
settings
,
)
is_practice_exam
=
Boolean
(
display_name
=
_
(
"Is Practice Exam"
),
help
=
_
(
...
...
@@ -177,90 +178,157 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
raise
NotFoundError
(
'Unexpected dispatch type'
)
@classmethod
def
verify_current_content_visibility
(
cls
,
due
,
hide_after_due
):
"""
Returns whether the content visibility policy passes
for the given due date and hide_after_due values and
the current date-time.
"""
return
(
not
due
or
not
hide_after_due
or
datetime
.
now
(
UTC
())
<
due
)
def
student_view
(
self
,
context
):
context
=
context
or
{}
self
.
_capture_basic_metrics
()
banner_text
=
None
special_html_view
=
self
.
_hidden_content_student_view
(
context
)
or
self
.
_special_exam_student_view
()
if
special_html_view
:
masquerading_as_specific_student
=
context
.
get
(
'specific_masquerade'
,
False
)
banner_text
,
special_html
=
special_html_view
if
special_html
and
not
masquerading_as_specific_student
:
return
Fragment
(
special_html
)
return
self
.
_student_view
(
context
,
banner_text
)
def
_special_exam_student_view
(
self
):
"""
Checks whether this sequential is a special exam. If so, returns
a banner_text or the fragment to display depending on whether
staff is masquerading.
"""
if
self
.
is_time_limited
:
special_exam_html
=
self
.
_time_limited_student_view
()
if
special_exam_html
:
banner_text
=
_
(
"This exam is hidden from the learner."
)
return
banner_text
,
special_exam_html
def
_hidden_content_student_view
(
self
,
context
):
"""
Checks whether the content of this sequential is hidden from the
runtime user. If so, returns a banner_text or the fragment to
display depending on whether staff is masquerading.
"""
if
not
self
.
_can_user_view_content
():
subsection_format
=
(
self
.
format
or
_
(
"subsection"
))
.
lower
()
# pylint: disable=no-member
# Translators: subsection_format refers to the assignment
# type of the subsection, such as Homework, Lab, Exam, etc.
banner_text
=
_
(
"Because the due date has passed, "
"this {subsection_format} is hidden from the learner."
)
.
format
(
subsection_format
=
subsection_format
)
hidden_content_html
=
self
.
system
.
render_template
(
'hidden_content.html'
,
{
'subsection_format'
:
subsection_format
,
'progress_url'
:
context
.
get
(
'progress_url'
),
}
)
return
banner_text
,
hidden_content_html
def
_can_user_view_content
(
self
):
"""
Returns whether the runtime user can view the content
of this sequential.
"""
return
(
self
.
runtime
.
user_is_staff
or
self
.
verify_current_content_visibility
(
self
.
due
,
self
.
hide_after_due
)
)
def
_student_view
(
self
,
context
,
banner_text
=
None
):
"""
Returns the rendered student view of the content of this
sequential. If banner_text is given, it is added to the
content.
"""
display_items
=
self
.
get_display_items
()
self
.
_update_position
(
context
,
len
(
display_items
))
fragment
=
Fragment
()
params
=
{
'items'
:
self
.
_render_student_view_for_items
(
context
,
display_items
,
fragment
),
'element_id'
:
self
.
location
.
html_id
(),
'item_id'
:
self
.
location
.
to_deprecated_string
(),
'position'
:
self
.
position
,
'tag'
:
self
.
location
.
category
,
'ajax_url'
:
self
.
system
.
ajax_url
,
'next_url'
:
context
.
get
(
'next_url'
),
'prev_url'
:
context
.
get
(
'prev_url'
),
'banner_text'
:
banner_text
,
}
fragment
.
add_content
(
self
.
system
.
render_template
(
"seq_module.html"
,
params
))
self
.
_capture_full_seq_item_metrics
(
display_items
)
self
.
_capture_current_unit_metrics
(
display_items
)
return
fragment
def
_update_position
(
self
,
context
,
number_of_display_items
):
"""
Update the user's sequential position given the context and the
number_of_display_items
"""
# If we're rendering this sequence, but no position is set yet,
# or exceeds the length of the displayable items,
# default the position to the first element
if
context
.
get
(
'requested_child'
)
==
'first'
:
self
.
position
=
1
elif
context
.
get
(
'requested_child'
)
==
'last'
:
self
.
position
=
len
(
display_items
)
or
1
elif
self
.
position
is
None
or
self
.
position
>
len
(
display_items
)
:
self
.
position
=
number_of_display_items
or
1
elif
self
.
position
is
None
or
self
.
position
>
number_of_display_items
:
self
.
position
=
1
## Returns a set of all types of all sub-children
contents
=
[]
fragment
=
Fragment
()
context
=
context
or
{}
def
_render_student_view_for_items
(
self
,
context
,
display_items
,
fragment
):
"""
Updates the given fragment with rendered student views of the given
display_items. Returns a list of dict objects with information about
the given display_items.
"""
bookmarks_service
=
self
.
runtime
.
service
(
self
,
"bookmarks"
)
context
[
"username"
]
=
self
.
runtime
.
service
(
self
,
"user"
)
.
get_current_user
()
.
opt_attrs
[
'edx-platform.username'
]
parent_module
=
self
.
get_parent
()
display_names
=
[
parent_module
.
display_name_with_default
,
self
.
get_parent
()
.
display_name_with_default
,
self
.
display_name_with_default
]
# We do this up here because proctored exam functionality could bypass
# rendering after this section.
self
.
_capture_basic_metrics
()
# Is this sequential part of a timed or proctored exam?
masquerading
=
context
.
get
(
'specific_masquerade'
,
False
)
special_exam_html
=
None
if
self
.
is_time_limited
:
special_exam_html
=
self
.
_time_limited_student_view
(
context
)
# Do we have an applicable alternate rendering
# from the edx_proctoring subsystem?
if
special_exam_html
and
not
masquerading
:
fragment
.
add_content
(
special_exam_html
)
return
fragment
for
child
in
display_items
:
is_bookmarked
=
bookmarks_service
.
is_bookmarked
(
usage_key
=
child
.
scope_ids
.
usage_id
)
contents
=
[]
for
item
in
display_items
:
is_bookmarked
=
bookmarks_service
.
is_bookmarked
(
usage_key
=
item
.
scope_ids
.
usage_id
)
context
[
"bookmarked"
]
=
is_bookmarked
progress
=
child
.
get_progress
()
rendered_
child
=
child
.
render
(
STUDENT_VIEW
,
context
)
fragment
.
add_frag_resources
(
rendered_
child
)
progress
=
item
.
get_progress
()
rendered_
item
=
item
.
render
(
STUDENT_VIEW
,
context
)
fragment
.
add_frag_resources
(
rendered_
item
)
child
info
=
{
'content'
:
rendered_
child
.
content
,
'page_title'
:
getattr
(
child
,
'tooltip_title'
,
''
),
item
info
=
{
'content'
:
rendered_
item
.
content
,
'page_title'
:
getattr
(
item
,
'tooltip_title'
,
''
),
'progress_status'
:
Progress
.
to_js_status_str
(
progress
),
'progress_detail'
:
Progress
.
to_js_detail_str
(
progress
),
'type'
:
child
.
get_icon_class
(),
'id'
:
child
.
scope_ids
.
usage_id
.
to_deprecated_string
(),
'type'
:
item
.
get_icon_class
(),
'id'
:
item
.
scope_ids
.
usage_id
.
to_deprecated_string
(),
'bookmarked'
:
is_bookmarked
,
'path'
:
" > "
.
join
(
display_names
+
[
child
.
display_name_with_default
]),
'path'
:
" > "
.
join
(
display_names
+
[
item
.
display_name_with_default
]),
}
contents
.
append
(
child
info
)
contents
.
append
(
item
info
)
params
=
{
'items'
:
contents
,
'element_id'
:
self
.
location
.
html_id
(),
'item_id'
:
self
.
location
.
to_deprecated_string
(),
'position'
:
self
.
position
,
'tag'
:
self
.
location
.
category
,
'ajax_url'
:
self
.
system
.
ajax_url
,
'next_url'
:
context
.
get
(
'next_url'
),
'prev_url'
:
context
.
get
(
'prev_url'
),
'override_hidden_exam'
:
masquerading
and
special_exam_html
is
not
None
,
}
fragment
.
add_content
(
self
.
system
.
render_template
(
"seq_module.html"
,
params
))
self
.
_capture_full_seq_item_metrics
(
display_items
)
self
.
_capture_current_unit_metrics
(
display_items
)
# Get all descendant XBlock types and counts
return
fragment
return
contents
def
_locations_in_subtree
(
self
,
node
):
"""
...
...
@@ -328,7 +396,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
for
block_type
,
count
in
curr_block_counts
.
items
():
newrelic
.
agent
.
add_custom_parameter
(
'seq.current.block_counts.{}'
.
format
(
block_type
),
count
)
def
_time_limited_student_view
(
self
,
context
):
def
_time_limited_student_view
(
self
):
"""
Delegated rendering of a student view when in a time
limited view. This ultimately calls down into edx_proctoring
...
...
common/lib/xmodule/xmodule/tests/test_sequence.py
View file @
9d747fa6
...
...
@@ -2,6 +2,10 @@
Tests for sequence module.
"""
# pylint: disable=no-member
from
datetime
import
timedelta
import
ddt
from
django.utils.timezone
import
now
from
freezegun
import
freeze_time
from
mock
import
Mock
from
xblock.reference.user_service
import
XBlockUser
,
UserService
from
xmodule.tests
import
get_test_system
...
...
@@ -24,10 +28,15 @@ class StubUserService(UserService):
return
user
@ddt.ddt
class
SequenceBlockTestCase
(
XModuleXmlImportTest
):
"""
Tests for the Sequence Module.
"""
TODAY
=
now
()
TOMORROW
=
TODAY
+
timedelta
(
days
=
1
)
DAY_AFTER_TOMORROW
=
TOMORROW
+
timedelta
(
days
=
1
)
@classmethod
def
setUpClass
(
cls
):
super
(
SequenceBlockTestCase
,
cls
)
.
setUpClass
()
...
...
@@ -54,13 +63,16 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
chapter_1
=
xml
.
ChapterFactory
.
build
(
parent
=
course
)
# has 2 child sequences
xml
.
ChapterFactory
.
build
(
parent
=
course
)
# has 0 child sequences
chapter_3
=
xml
.
ChapterFactory
.
build
(
parent
=
course
)
# has 1 child sequence
chapter_4
=
xml
.
ChapterFactory
.
build
(
parent
=
course
)
# has
2 child sequences
chapter_4
=
xml
.
ChapterFactory
.
build
(
parent
=
course
)
# has
1 child sequence, with hide_after_due
xml
.
SequenceFactory
.
build
(
parent
=
chapter_1
)
xml
.
SequenceFactory
.
build
(
parent
=
chapter_1
)
sequence_3_1
=
xml
.
SequenceFactory
.
build
(
parent
=
chapter_3
)
# has 3 verticals
xml
.
SequenceFactory
.
build
(
parent
=
chapter_4
)
xml
.
SequenceFactory
.
build
(
parent
=
chapter_4
)
xml
.
SequenceFactory
.
build
(
# sequence_4_1
parent
=
chapter_4
,
hide_after_due
=
str
(
True
),
due
=
str
(
cls
.
TOMORROW
),
)
for
_
in
range
(
3
):
xml
.
VerticalFactory
.
build
(
parent
=
sequence_3_1
)
...
...
@@ -98,9 +110,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
def
test_render_student_view
(
self
):
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_3_1
,
requested_child
=
None
,
next_url
=
'NextSequential'
,
prev_url
=
'PrevSequential'
extra_context
=
dict
(
next_url
=
'NextSequential'
,
prev_url
=
'PrevSequential'
),
)
self
.
_assert_view_at_position
(
html
,
expected_position
=
1
)
self
.
assertIn
(
unicode
(
self
.
sequence_3_1
.
location
),
html
)
...
...
@@ -115,20 +125,15 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_3_1
,
requested_child
=
'last'
)
self
.
_assert_view_at_position
(
html
,
expected_position
=
3
)
def
_get_rendered_student_view
(
self
,
sequence
,
requested_child
,
next_url
=
None
,
prev_url
=
None
):
def
_get_rendered_student_view
(
self
,
sequence
,
requested_child
=
None
,
extra_context
=
None
):
"""
Returns the rendered student view for the given sequence and the
requested_child parameter.
"""
return
sequence
.
xmodule_runtime
.
render
(
sequence
,
STUDENT_VIEW
,
{
'requested_child'
:
requested_child
,
'next_url'
:
next_url
,
'prev_url'
:
prev_url
,
},
)
.
content
context
=
{
'requested_child'
:
requested_child
}
if
extra_context
:
context
.
update
(
extra_context
)
return
sequence
.
xmodule_runtime
.
render
(
sequence
,
STUDENT_VIEW
,
context
)
.
content
def
_assert_view_at_position
(
self
,
rendered_html
,
expected_position
):
"""
...
...
@@ -140,3 +145,46 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_3_1
,
requested_child
=
None
)
for
child
in
self
.
sequence_3_1
.
children
:
self
.
assertIn
(
"'page_title': '{}'"
.
format
(
child
.
name
),
html
)
def
test_hidden_content_before_due
(
self
):
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_4_1
)
self
.
assertIn
(
"seq_module.html"
,
html
)
self
.
assertIn
(
"'banner_text': None"
,
html
)
@freeze_time
(
DAY_AFTER_TOMORROW
)
@ddt.data
(
(
None
,
'subsection'
),
(
'Homework'
,
'homework'
),
)
@ddt.unpack
def
test_hidden_content_past_due
(
self
,
format_type
,
expected_text
):
progress_url
=
'http://test_progress_link'
self
.
_set_sequence_format
(
self
.
sequence_4_1
,
format_type
)
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_4_1
,
extra_context
=
dict
(
progress_url
=
progress_url
),
)
self
.
assertIn
(
"hidden_content.html"
,
html
)
self
.
assertIn
(
progress_url
,
html
)
self
.
assertIn
(
"'subsection_format': '{}'"
.
format
(
expected_text
),
html
)
@freeze_time
(
DAY_AFTER_TOMORROW
)
def
test_masquerade_hidden_content_past_due
(
self
):
self
.
_set_sequence_format
(
self
.
sequence_4_1
,
"Homework"
)
html
=
self
.
_get_rendered_student_view
(
self
.
sequence_4_1
,
extra_context
=
dict
(
specific_masquerade
=
True
),
)
self
.
assertIn
(
"seq_module.html"
,
html
)
self
.
assertIn
(
"'banner_text': 'Because the due date has passed, "
"this homework is hidden from the learner.'"
,
html
)
def
_set_sequence_format
(
self
,
sequence
,
format_type
):
"""
Sets the format field on the given sequence to the
given value.
"""
sequence
.
_xmodule
.
format
=
format_type
# pylint: disable=protected-access
lms/djangoapps/course_api/blocks/api.py
View file @
9d747fa6
...
...
@@ -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.transformers.hidden_content
import
HiddenContentTransformer
from
openedx.core.lib.block_structure.transformers
import
BlockStructureTransformers
from
.transformers.blocks_api
import
BlocksAPITransformer
...
...
@@ -51,7 +52,7 @@ def get_blocks(
# create ordered list of transformers, adding BlocksAPITransformer at end.
transformers
=
BlockStructureTransformers
()
if
user
is
not
None
:
transformers
+=
COURSE_BLOCK_ACCESS_TRANSFORMERS
+
[
ProctoredExamTransformer
()]
transformers
+=
COURSE_BLOCK_ACCESS_TRANSFORMERS
+
[
ProctoredExamTransformer
()
,
HiddenContentTransformer
()
]
transformers
+=
[
BlocksAPITransformer
(
block_counts
,
...
...
lms/djangoapps/course_blocks/api.py
View file @
9d747fa6
...
...
@@ -2,11 +2,8 @@
API entry point to the course_blocks app with top-level
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.lib.block_structure.manager
import
BlockStructureManager
from
openedx.core.lib.block_structure.transformers
import
BlockStructureTransformers
from
xmodule.modulestore.django
import
modulestore
from
.transformers
import
(
library_content
,
...
...
lms/djangoapps/course_blocks/transformers/hidden_content.py
0 → 100644
View file @
9d747fa6
"""
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
)
lms/djangoapps/course_blocks/transformers/start_date.py
View file @
9d747fa6
...
...
@@ -5,7 +5,7 @@ from openedx.core.lib.block_structure.transformer import BlockStructureTransform
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
from
.utils
import
collect_merged_date_field
class
StartDateTransformer
(
FilteringTransformerMixin
,
BlockStructureTransformer
):
...
...
@@ -36,7 +36,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
return
"start_date"
@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
the given block_key in the given block_structure.
...
...
@@ -53,35 +53,15 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
"""
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
)
collect_merged_date_field
(
block_structure
,
transformer
=
cls
,
xblock_field_name
=
'start'
,
merged_field_name
=
cls
.
MERGED_START_DATE
,
default_date
=
DEFAULT_START_DATE
,
func_merge_parents
=
min
,
func_merge_ancestors
=
max
,
)
def
transform_block_filters
(
self
,
usage_info
,
block_structure
):
# Users with staff access bypass the Start Date check.
...
...
@@ -91,7 +71,7 @@ class StartDateTransformer(FilteringTransformerMixin, BlockStructureTransformer)
removal_condition
=
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
),
self
.
_
get_merged_start_date
(
block_structure
,
block_key
),
usage_info
.
course_key
,
)
return
[
block_structure
.
create_removal_filter
(
removal_condition
)]
lms/djangoapps/course_blocks/transformers/tests/helpers.py
View file @
9d747fa6
...
...
@@ -255,7 +255,7 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
self
,
test_user
,
expected_user_accessible_blocks
,
blocks_with_differing_access
,
blocks_with_differing_access
=
None
,
transformers
=
None
,
):
"""
...
...
@@ -272,7 +272,8 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
blocks_with_differing_access (set(int)): Set of
blocks (indices) whose access will differ from the
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
of transformers that are to be executed. If not
...
...
@@ -312,7 +313,6 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
# compute access results of the block
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
self
.
assertEquals
(
...
...
@@ -323,23 +323,25 @@ class BlockParentsMapTestCase(TransformerRegistryTestMixin, ModuleStoreTestCase)
)
)
# compare with has_access_result
if
i
in
blocks_with_differing_access
:
self
.
assertNotEqual
(
block_structure_result
,
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
if
blocks_with_differing_access
:
# compare with has_access_result
has_access_result
=
bool
(
has_access
(
user
,
'load'
,
self
.
get_block
(
i
),
course_key
=
self
.
course
.
id
))
if
i
in
blocks_with_differing_access
:
self
.
assertNotEqual
(
block_structure_result
,
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
:
self
.
assertEquals
(
block_structure
_result
,
has_access_result
,
"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
else
:
self
.
assertEquals
(
block_structure_result
,
has_access
_result
,
"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
)
)
)
self
.
client
.
logout
()
...
...
lms/djangoapps/course_blocks/transformers/tests/test_hidden_content.py
0 → 100644
View file @
9d747fa6
"""
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
,
)
lms/djangoapps/course_blocks/transformers/utils.py
View file @
9d747fa6
...
...
@@ -10,7 +10,103 @@ def get_field_on_block(block, field_name, default_value=None):
returns value from only a single parent chain
(e.g., doesn't take a union in DAGs).
"""
if
block
.
fields
[
field_name
]
.
is_set_on
(
block
):
return
getattr
(
block
,
field_name
)
else
:
return
default_value
try
:
if
block
.
fields
[
field_name
]
.
is_set_on
(
block
):
return
getattr
(
block
,
field_name
)
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
)
lms/djangoapps/course_blocks/transformers/visibility.py
View file @
9d747fa6
...
...
@@ -2,6 +2,7 @@
Visibility Transformer implementation.
"""
from
openedx.core.lib.block_structure.transformer
import
BlockStructureTransformer
,
FilteringTransformerMixin
from
.utils
import
collect_merged_boolean_field
class
VisibilityTransformer
(
FilteringTransformerMixin
,
BlockStructureTransformer
):
...
...
@@ -30,7 +31,7 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
return
"visibility"
@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
given block_structure should be visible to staff only per
...
...
@@ -46,26 +47,12 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
Collects any information that's necessary to execute this
transformer's transform method.
"""
for
block_key
in
block_structure
.
topological_traversal
():
# compute merged value of visible_to_staff_only from all parents
parents
=
block_structure
.
get_parents
(
block_key
)
all_parents_visible_to_staff_only
=
all
(
# pylint: disable=invalid-name
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
)
)
collect_merged_boolean_field
(
block_structure
,
transformer
=
cls
,
xblock_field_name
=
'visible_to_staff_only'
,
merged_field_name
=
cls
.
MERGED_VISIBLE_TO_STAFF_ONLY
,
)
def
transform_block_filters
(
self
,
usage_info
,
block_structure
):
# Users with staff access bypass the Visibility check.
...
...
@@ -74,6 +61,6 @@ class VisibilityTransformer(FilteringTransformerMixin, BlockStructureTransformer
return
[
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
),
)
]
lms/djangoapps/courseware/views/index.py
View file @
9d747fa6
...
...
@@ -447,7 +447,7 @@ class CoursewareIndex(View):
return
"{url}?child={requested_child}"
.
format
(
url
=
reverse
(
'courseware_section'
,
args
=
[
unicode
(
self
.
course
.
id
),
section_info
[
'chapter_url_name'
],
section_info
[
'url_name'
]],
args
=
[
unicode
(
self
.
course
_key
),
section_info
[
'chapter_url_name'
],
section_info
[
'url_name'
]],
),
requested_child
=
requested_child
,
)
...
...
@@ -455,6 +455,7 @@ class CoursewareIndex(View):
section_context
=
{
'activate_block_id'
:
self
.
request
.
GET
.
get
(
'activate_block_id'
),
'requested_child'
:
self
.
request
.
GET
.
get
(
"child"
),
'progress_url'
:
reverse
(
'progress'
,
kwargs
=
{
'course_id'
:
unicode
(
self
.
course_key
)}),
}
if
previous_of_active_section
:
section_context
[
'prev_url'
]
=
_compute_section_url
(
previous_of_active_section
,
'last'
)
...
...
lms/templates/hidden_content.html
0 → 100644
View file @
9d747fa6
<
%
page
expression_filter=
"h"
/>
<
%!
from
django
.
utils
.
translation
import
ugettext
as
_
from
openedx
.
core
.
djangolib
.
markup
import
HTML
,
Text
%
>
<div
class=
"sequence hidden-content proctored-exam completed"
>
<h3>
${_("The due date for this {subsection_format} has passed.").format(
subsection_format=subsection_format,
)}
</h3>
<hr>
<p>
${Text(_(
"Because the due date has passed, this {subsection_format} "
"is no longer available.{line_break}If you have completed this {subsection_format}, "
"your grade is available on the {link_start}progress page{link_end}."
)).format(
subsection_format=subsection_format,
line_break=HTML("
<br>
"),
link_start=HTML("
<a
href=
'{}'
>
").format(progress_url),
link_end=HTML("
</a>
"),
)}
</p>
</div>
lms/templates/seq_module.html
View file @
9d747fa6
...
...
@@ -3,12 +3,12 @@
<div
id=
"sequence_${element_id}"
class=
"sequence"
data-id=
"${item_id}"
data-position=
"${position}"
data-ajax-url=
"${ajax_url}"
data-next-url=
"${next_url}"
data-prev-url=
"${prev_url}"
>
<div
class=
"path"
></div>
% if
override_hidden_exam
:
% if
banner_text
:
<div
class=
"pattern-library-shim alert alert-information subsection-header"
tabindex=
"-1"
>
<span
class=
"pattern-library-shim icon alert-icon fa fa-bullhorn"
aria-hidden=
"true"
></span>
<div
class=
"pattern-library-shim alert-message"
>
<p
class=
"pattern-library-shim alert-copy"
>
${
_("This exam is hidden from the learner.")
}
${
banner_text
}
</p>
</div>
</div>
...
...
setup.py
View file @
9d747fa6
...
...
@@ -49,6 +49,7 @@ setup(
"start_date = lms.djangoapps.course_blocks.transformers.start_date:StartDateTransformer"
,
"user_partitions = lms.djangoapps.course_blocks.transformers.user_partitions:UserPartitionTransformer"
,
"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"
,
"proctored_exam = lms.djangoapps.course_api.blocks.transformers.proctored_exam:ProctoredExamTransformer"
,
"grades = lms.djangoapps.courseware.transformers.grades:GradesTransformer"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment