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
d447c075
Commit
d447c075
authored
Jul 10, 2014
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support staff locking on the unit page
STUD-1873
parent
cf70eb6e
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
920 additions
and
418 deletions
+920
-418
cms/djangoapps/contentstore/tests/test_utils.py
+3
-3
cms/djangoapps/contentstore/utils.py
+3
-2
cms/djangoapps/contentstore/views/component.py
+1
-2
cms/djangoapps/contentstore/views/item.py
+62
-53
cms/djangoapps/contentstore/views/tests/test_container_page.py
+0
-36
cms/djangoapps/contentstore/views/tests/test_item.py
+49
-0
cms/static/js/models/xblock_info.js
+7
-2
cms/static/js/spec/views/pages/container_spec.js
+26
-26
cms/static/js/spec/views/pages/container_subviews_spec.js
+252
-106
cms/static/js/views/pages/container.js
+5
-0
cms/static/js/views/pages/container_subviews.js
+94
-14
cms/static/js/views/utils/view_utils.js
+4
-1
cms/static/sass/_developer.scss
+1
-2
cms/static/sass/views/_container.scss
+9
-106
cms/templates/container.html
+2
-11
cms/templates/js/container-message.underscore
+8
-0
cms/templates/js/course-outline.underscore
+2
-2
cms/templates/js/mock/mock-container-page.underscore
+1
-0
cms/templates/js/publish-xblock.underscore
+53
-30
common/test/acceptance/pages/lms/staff_view.py
+10
-2
common/test/acceptance/pages/studio/container.py
+43
-0
common/test/acceptance/tests/test_lms.py
+85
-0
common/test/acceptance/tests/test_studio_container.py
+200
-20
No files found.
cms/djangoapps/contentstore/tests/test_utils.py
View file @
d447c075
...
...
@@ -231,7 +231,7 @@ class XBlockVisibilityTestCase(TestCase):
vertical
.
start
=
self
.
future
modulestore
()
.
update_item
(
vertical
,
self
.
dummy_user
)
self
.
assertTrue
(
utils
.
is_
xblock
_visible_to_students
(
vertical
))
self
.
assertTrue
(
utils
.
is_
currently
_visible_to_students
(
vertical
))
def
_test_visible_to_students
(
self
,
expected_visible_without_lock
,
name
,
start_date
,
publish
=
False
):
"""
...
...
@@ -239,13 +239,13 @@ class XBlockVisibilityTestCase(TestCase):
with and without visible_to_staff_only set.
"""
no_staff_lock
=
self
.
_create_xblock_with_start_date
(
name
,
start_date
,
publish
,
visible_to_staff_only
=
False
)
self
.
assertEqual
(
expected_visible_without_lock
,
utils
.
is_
xblock
_visible_to_students
(
no_staff_lock
))
self
.
assertEqual
(
expected_visible_without_lock
,
utils
.
is_
currently
_visible_to_students
(
no_staff_lock
))
# any xblock with visible_to_staff_only set to True should not be visible to students.
staff_lock
=
self
.
_create_xblock_with_start_date
(
name
+
"_locked"
,
start_date
,
publish
,
visible_to_staff_only
=
True
)
self
.
assertFalse
(
utils
.
is_
xblock
_visible_to_students
(
staff_lock
))
self
.
assertFalse
(
utils
.
is_
currently
_visible_to_students
(
staff_lock
))
def
_create_xblock_with_start_date
(
self
,
name
,
start_date
,
publish
=
False
,
visible_to_staff_only
=
False
):
"""Helper to create an xblock with a start date, optionally publishing it"""
...
...
cms/djangoapps/contentstore/utils.py
View file @
d447c075
...
...
@@ -164,9 +164,10 @@ def compute_publish_state(xblock):
return
modulestore
()
.
compute_publish_state
(
xblock
)
def
is_
xblock
_visible_to_students
(
xblock
):
def
is_
currently
_visible_to_students
(
xblock
):
"""
Returns true if there is a published version of the xblock that has been released.
Returns true if there is a published version of the xblock that is currently visible to students.
This means that it has a release date in the past, and the xblock has not been set to staff only.
"""
try
:
...
...
cms/djangoapps/contentstore/views/component.py
View file @
d447c075
...
...
@@ -21,7 +21,7 @@ from xblock.fields import Scope
from
xblock.plugin
import
PluginMissingError
from
xblock.runtime
import
Mixologist
from
contentstore.utils
import
get_lms_link_for_item
,
compute_publish_state
,
is_xblock_visible_to_students
from
contentstore.utils
import
get_lms_link_for_item
,
compute_publish_state
from
contentstore.views.helpers
import
get_parent_xblock
,
is_unit
,
xblock_type_display_name
from
contentstore.views.item
import
create_xblock_info
...
...
@@ -207,7 +207,6 @@ def container_handler(request, usage_key_string):
'xblock_locator'
:
xblock
.
location
,
'unit'
:
unit
,
'is_unit_page'
:
is_unit_page
,
'is_visible_to_students'
:
is_xblock_visible_to_students
(
xblock
),
'subsection'
:
subsection
,
'section'
:
section
,
'new_unit_category'
:
'vertical'
,
...
...
cms/djangoapps/contentstore/views/item.py
View file @
d447c075
...
...
@@ -37,6 +37,7 @@ from util.date_utils import get_default_time_display
from
util.json_request
import
expect_json
,
JsonResponse
from
.access
import
has_course_access
from
contentstore.utils
import
is_currently_visible_to_students
from
contentstore.views.helpers
import
is_unit
,
xblock_studio_url
,
xblock_primary_child_category
,
\
xblock_type_display_name
,
get_parent_xblock
from
contentstore.views.preview
import
get_preview_fragment
...
...
@@ -104,10 +105,12 @@ def xblock_handler(request, usage_key_string):
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
:graderType: change how this unit is graded
:publish: can be either -- 'make_public' (which publishes the content) or 'discard_changes'
(which reverts to the last published version). If 'discard_changes', the other fields
will not be used; that is, it is not possible to update and discard changes
in a single operation.
:publish: can be:
'make_public': publish the content
'republish': publish this item *only* if it was previously published
'discard_changes' - reverts to the last published version
Note: If 'discard_changes', the other fields will not be used; that is, it is not possible
to update and discard changes in a single operation.
The JSON representation on the updated xblock (minus children) is returned.
if usage_key_string is not specified, create a new xblock instance, either by duplicating
...
...
@@ -136,7 +139,7 @@ def xblock_handler(request, usage_key_string):
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return
JsonResponse
(
CourseGradingModel
.
get_section_grader_type
(
usage_key
))
# TODO: pass fields to _get_module_info and only return those
rsp
=
_get_module_info
(
usage_key
,
request
.
user
)
rsp
=
_get_module_info
(
_get_xblock
(
usage_key
,
request
.
user
)
)
return
JsonResponse
(
rsp
)
else
:
return
HttpResponse
(
status
=
406
)
...
...
@@ -145,9 +148,9 @@ def xblock_handler(request, usage_key_string):
_delete_item
(
usage_key
,
request
.
user
)
return
JsonResponse
()
else
:
# Since we have a usage_key, we are updating an existing xblock.
return
_save_
item
(
return
_save_
xblock
(
request
.
user
,
usage_key
,
_get_xblock
(
usage_key
,
request
.
user
)
,
data
=
request
.
json
.
get
(
'data'
),
children
=
request
.
json
.
get
(
'children'
),
metadata
=
request
.
json
.
get
(
'metadata'
),
...
...
@@ -289,8 +292,8 @@ def xblock_outline_handler(request, usage_key_string):
return
Http404
def
_save_
item
(
user
,
usage_key
,
data
=
None
,
children
=
None
,
metadata
=
None
,
nullout
=
None
,
grader_type
=
None
,
publish
=
None
):
def
_save_
xblock
(
user
,
xblock
,
data
=
None
,
children
=
None
,
metadata
=
None
,
nullout
=
None
,
grader_type
=
None
,
publish
=
None
):
"""
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
...
...
@@ -298,32 +301,19 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
"""
store
=
modulestore
()
try
:
existing_item
=
store
.
get_item
(
usage_key
)
except
ItemNotFoundError
:
if
usage_key
.
category
in
CREATE_IF_NOT_FOUND
:
# New module at this location, for pages that are not pre-created.
# Used for course info handouts.
existing_item
=
store
.
create_item
(
user
.
id
,
usage_key
.
course_key
,
usage_key
.
block_type
,
usage_key
.
block_id
)
else
:
raise
except
InvalidLocationError
:
log
.
error
(
"Can't find item by location."
)
return
JsonResponse
({
"error"
:
"Can't find item by location: "
+
unicode
(
usage_key
)},
404
)
# Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
if
publish
==
"discard_changes"
:
store
.
revert_to_published
(
usage_key
,
user
.
id
)
store
.
revert_to_published
(
xblock
.
location
,
user
.
id
)
# Returning the same sort of result that we do for other save operations. In the future,
# we may want to return the full XBlockInfo.
return
JsonResponse
({
'id'
:
unicode
(
usage_key
)})
return
JsonResponse
({
'id'
:
unicode
(
xblock
.
location
)})
old_metadata
=
own_metadata
(
existing_item
)
old_content
=
existing_item
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
old_metadata
=
own_metadata
(
xblock
)
old_content
=
xblock
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
if
data
:
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
existing_item
.
data
=
data
xblock
.
data
=
data
else
:
data
=
old_content
[
'data'
]
if
'data'
in
old_content
else
None
...
...
@@ -332,7 +322,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
for
child
in
children
:
child_usage_key
=
usage_key_with_run
(
child
)
children_usage_keys
.
append
(
child_usage_key
)
existing_item
.
children
=
children_usage_keys
xblock
.
children
=
children_usage_keys
# also commit any metadata which might have been passed along
if
nullout
is
not
None
or
metadata
is
not
None
:
...
...
@@ -341,53 +331,61 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
# 'apply' the submitted metadata, so we don't end up deleting system metadata.
if
nullout
is
not
None
:
for
metadata_key
in
nullout
:
setattr
(
existing_item
,
metadata_key
,
None
)
setattr
(
xblock
,
metadata_key
,
None
)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
# the intent is to make it None, use the nullout field
if
metadata
is
not
None
:
for
metadata_key
,
value
in
metadata
.
items
():
field
=
existing_item
.
fields
[
metadata_key
]
field
=
xblock
.
fields
[
metadata_key
]
if
value
is
None
:
field
.
delete_from
(
existing_item
)
field
.
delete_from
(
xblock
)
else
:
try
:
value
=
field
.
from_json
(
value
)
except
ValueError
:
return
JsonResponse
({
"error"
:
"Invalid data"
},
400
)
field
.
write_to
(
existing_item
,
value
)
field
.
write_to
(
xblock
,
value
)
if
callable
(
getattr
(
existing_item
,
"editor_saved"
,
None
)):
existing_item
.
editor_saved
(
user
,
old_metadata
,
old_content
)
if
callable
(
getattr
(
xblock
,
"editor_saved"
,
None
)):
xblock
.
editor_saved
(
user
,
old_metadata
,
old_content
)
# commit to datastore
store
.
update_item
(
existing_item
,
user
.
id
)
store
.
update_item
(
xblock
,
user
.
id
)
# for static tabs, their containing course also records their display name
if
usage_key
.
category
==
'static_tab'
:
course
=
store
.
get_course
(
usage_key
.
course_key
)
if
xblock
.
location
.
category
==
'static_tab'
:
course
=
store
.
get_course
(
xblock
.
location
.
course_key
)
# find the course's reference to this tab and update the name.
static_tab
=
CourseTabList
.
get_tab_by_slug
(
course
.
tabs
,
usage_key
.
name
)
static_tab
=
CourseTabList
.
get_tab_by_slug
(
course
.
tabs
,
xblock
.
location
.
name
)
# only update if changed
if
static_tab
and
static_tab
[
'name'
]
!=
existing_item
.
display_name
:
static_tab
[
'name'
]
=
existing_item
.
display_name
if
static_tab
and
static_tab
[
'name'
]
!=
xblock
.
display_name
:
static_tab
[
'name'
]
=
xblock
.
display_name
store
.
update_item
(
course
,
user
.
id
)
result
=
{
'id'
:
unicode
(
usage_key
),
'id'
:
unicode
(
xblock
.
location
),
'data'
:
data
,
'metadata'
:
own_metadata
(
existing_item
)
'metadata'
:
own_metadata
(
xblock
)
}
if
grader_type
is
not
None
:
result
.
update
(
CourseGradingModel
.
update_section_grader_type
(
existing_item
,
grader_type
,
user
))
result
.
update
(
CourseGradingModel
.
update_section_grader_type
(
xblock
,
grader_type
,
user
))
# If publish is set to 'republish' and this item has previously been published, then this
# new item should be republished. This is used by staff locking to ensure that changing the draft
# value of the staff lock will also update the published version.
if
publish
==
'republish'
:
published
=
modulestore
()
.
has_item
(
xblock
.
location
,
revision
=
ModuleStoreEnum
.
RevisionOption
.
published_only
)
if
published
:
publish
=
'make_public'
# Make public after updating the xblock, in case the caller asked for both an update and a publish.
#
Although not supported in the UI, Bok Choy tests use thi
s.
#
Used by Bok Choy tests and by republishing of staff lock
s.
if
publish
==
'make_public'
:
modulestore
()
.
publish
(
existing_item
.
location
,
user
.
id
)
modulestore
()
.
publish
(
xblock
.
location
,
user
.
id
)
# Note that children aren't being returned until we have a use case.
return
JsonResponse
(
result
)
...
...
@@ -552,31 +550,40 @@ def orphan_handler(request, course_key_string):
raise
PermissionDenied
()
def
_get_
module_info
(
usage_key
,
user
,
rewrite_static_links
=
True
):
def
_get_
xblock
(
usage_key
,
user
):
"""
metadata, data, id representation of a leaf module fetcher.
:param usage_key: A UsageKey
Returns the xblock for the specified usage key. Note: if failing to find a key with a category
in the CREATE_IF_NOT_FOUND list, an xblock will be created and saved automatically.
"""
store
=
modulestore
()
try
:
module
=
store
.
get_item
(
usage_key
)
return
store
.
get_item
(
usage_key
)
except
ItemNotFoundError
:
if
usage_key
.
category
in
CREATE_IF_NOT_FOUND
:
# Create a new one for certain categories only. Used for course info handouts.
module
=
store
.
create_item
(
user
.
id
,
usage_key
.
course_key
,
usage_key
.
block_type
,
block_id
=
usage_key
.
block_id
)
return
store
.
create_item
(
user
.
id
,
usage_key
.
course_key
,
usage_key
.
block_type
,
block_id
=
usage_key
.
block_id
)
else
:
raise
except
InvalidLocationError
:
log
.
error
(
"Can't find item by location."
)
return
JsonResponse
({
"error"
:
"Can't find item by location: "
+
unicode
(
usage_key
)},
404
)
data
=
getattr
(
module
,
'data'
,
''
)
def
_get_module_info
(
xblock
,
rewrite_static_links
=
True
):
"""
metadata, data, id representation of a leaf module fetcher.
:param usage_key: A UsageKey
"""
data
=
getattr
(
xblock
,
'data'
,
''
)
if
rewrite_static_links
:
data
=
replace_static_urls
(
data
,
None
,
course_id
=
module
.
location
.
course_key
course_id
=
xblock
.
location
.
course_key
)
# Note that children aren't being returned until we have a use case.
return
create_xblock_info
(
module
,
data
=
data
,
metadata
=
own_metadata
(
module
),
include_ancestor_info
=
True
)
return
create_xblock_info
(
xblock
,
data
=
data
,
metadata
=
own_metadata
(
xblock
),
include_ancestor_info
=
True
)
def
create_xblock_info
(
xblock
,
data
=
None
,
metadata
=
None
,
include_ancestor_info
=
False
,
include_child_info
=
False
,
...
...
@@ -630,6 +637,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"released_to_students"
:
datetime
.
now
(
UTC
)
>
xblock
.
start
,
"release_date"
:
release_date
,
"release_date_from"
:
_get_release_date_from
(
xblock
)
if
release_date
else
None
,
"visible_to_staff_only"
:
xblock
.
visible_to_staff_only
,
"currently_visible_to_students"
:
is_currently_visible_to_students
(
xblock
),
}
if
data
is
not
None
:
xblock_info
[
"data"
]
=
data
...
...
cms/djangoapps/contentstore/views/tests/test_container_page.py
View file @
d447c075
...
...
@@ -157,39 +157,3 @@ class ContainerPageTestCase(StudioPageTestCase):
"""
empty_child_container
=
self
.
_create_item
(
self
.
vertical
.
location
,
'split_test'
,
'Split Test'
)
self
.
validate_preview_html
(
empty_child_container
,
self
.
reorderable_child_view
,
can_add
=
False
)
def
test_unreleased_private_container_messages
(
self
):
"""
Verify that an unreleased private container does not display messages.
"""
self
.
validate_html_for_messages
(
self
.
unreleased_private_vertical
,
False
)
def
test_unreleased_public_container_messages
(
self
):
"""
Verify that an unreleased public container does not display messages.
"""
self
.
validate_html_for_messages
(
self
.
unreleased_public_vertical
,
False
)
def
test_released_private_container_message
(
self
):
"""
Verify that a released private container does not display messages.
"""
self
.
validate_html_for_messages
(
self
.
released_private_vertical
,
False
)
def
test_released_public_container_message
(
self
):
"""
Verify that a released public container does display messages.
"""
self
.
validate_html_for_messages
(
self
.
released_public_vertical
,
True
)
def
validate_html_for_messages
(
self
,
xblock
,
has_messages
):
"""
Validate that the specified HTML has the appropriate messages for the current student visibility state.
"""
# Verify that there are no warning messages for blocks that are not visible to students
html
=
self
.
get_page_html
(
xblock
)
messages_html
=
'<div class="container-message wrapper-message">'
if
has_messages
:
self
.
assertIn
(
messages_html
,
html
)
else
:
self
.
assertNotIn
(
messages_html
,
html
)
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
d447c075
...
...
@@ -567,6 +567,55 @@ class TestEditItem(ItemTest):
published
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
self
.
assertIsNone
(
published
.
due
)
def
test_republish
(
self
):
""" Test republishing an item. """
new_display_name
=
'New Display Name'
republish_data
=
{
'publish'
:
'republish'
,
'display_name'
:
new_display_name
}
# When the problem is first created, it is only in draft (because of its category).
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
# Republishing when only in draft will update the draft but not cause a public item to be created.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'republish'
,
'metadata'
:
{
'display_name'
:
new_display_name
}
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
draft
=
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
,
verify_is_draft
=
True
)
self
.
assertEqual
(
draft
.
display_name
,
new_display_name
)
# Publish the item
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
# Now republishing should update the published version
new_display_name_2
=
'New Display Name 2'
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'republish'
,
'metadata'
:
{
'display_name'
:
new_display_name_2
}
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
published
=
modulestore
()
.
get_item
(
self
.
problem_usage_key
,
revision
=
ModuleStoreEnum
.
RevisionOption
.
published_only
)
self
.
assertEqual
(
published
.
display_name
,
new_display_name_2
)
def
_make_draft_content_different_from_published
(
self
):
"""
Helper method to create different draft and published versions of a problem.
...
...
cms/static/js/models/xblock_info.js
View file @
d447c075
...
...
@@ -38,7 +38,7 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
* If true, only course staff can see the xblock regardless of publish status or
* release date status.
*/
"
locked
"
:
null
,
"
visible_to_staff_only
"
:
null
,
/**
* Date of the last edit to this xblock or any of its descendants.
*/
...
...
@@ -69,7 +69,12 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
* this will either be the parent subsection or the grandparent section.
* This can be null if the release date is unscheduled.
*/
"release_date_from"
:
null
"release_date_from"
:
null
,
/**
* True if this xblock is currently visible to students. This is computed server-side
* so that the logic isn't duplicated on the client.
*/
"currently_visible_to_students"
:
null
},
parse
:
function
(
response
)
{
...
...
cms/static/js/spec/views/pages/container_spec.js
View file @
d447c075
...
...
@@ -48,7 +48,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
);
};
renderContainerPage
=
function
(
html
,
test
,
options
)
{
renderContainerPage
=
function
(
test
,
html
,
options
)
{
requests
=
create_sinon
.
requests
(
test
);
containerPage
=
new
ContainerPage
(
_
.
extend
(
options
||
{},
{
model
:
model
,
...
...
@@ -70,7 +70,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe
(
"Initial display"
,
function
()
{
it
(
'can render itself'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
expect
(
containerPage
.
$
(
'.xblock-header'
).
length
).
toBe
(
9
);
expect
(
containerPage
.
$
(
'.wrapper-xblock .level-nesting'
)).
not
.
toHaveClass
(
'is-hidden'
);
});
...
...
@@ -84,7 +84,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'inline edits the display name when performing a new action'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
,
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
action
:
'new'
});
expect
(
containerPage
.
$
(
'.xblock-header'
).
length
).
toBe
(
9
);
...
...
@@ -106,8 +106,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
expectEditCanceled
=
function
(
test
,
options
)
{
var
initialRequests
,
displayNameWrapper
;
renderContainerPage
(
mockContainerXBlockHtml
,
test
);
var
initialRequests
,
displayNameWrapper
,
displayNameInput
;
renderContainerPage
(
test
,
mockContainerXBlockHtml
);
displayNameWrapper
=
getDisplayNameWrapper
();
initialRequests
=
requests
.
length
;
displayNameInput
=
edit_helpers
.
inlineEdit
(
displayNameWrapper
,
options
.
newTitle
);
...
...
@@ -125,7 +125,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'can edit itself'
,
function
()
{
var
editButtons
,
displayNameElement
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
displayNameElement
=
containerPage
.
$
(
'.page-header-title'
);
// Click the root edit button
...
...
@@ -162,7 +162,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'can inline edit the display name'
,
function
()
{
var
displayNameInput
,
displayNameWrapper
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
displayNameWrapper
=
getDisplayNameWrapper
();
displayNameInput
=
edit_helpers
.
inlineEdit
(
displayNameWrapper
,
updatedDisplayName
);
displayNameInput
.
change
();
...
...
@@ -176,7 +176,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'does not change the title when a display name update fails'
,
function
()
{
var
initialRequests
,
displayNameInput
,
displayNameWrapper
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
displayNameWrapper
=
getDisplayNameWrapper
();
displayNameInput
=
edit_helpers
.
inlineEdit
(
displayNameWrapper
,
updatedDisplayName
);
initialRequests
=
requests
.
length
;
...
...
@@ -190,7 +190,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'trims whitespace from the display name'
,
function
()
{
var
displayNameInput
,
displayNameWrapper
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
displayNameWrapper
=
getDisplayNameWrapper
();
displayNameInput
=
edit_helpers
.
inlineEdit
(
displayNameWrapper
,
updatedDisplayName
+
' '
);
displayNameInput
.
change
();
...
...
@@ -222,7 +222,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'can show an edit modal for a child xblock'
,
function
()
{
var
editButtons
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
editButtons
=
containerPage
.
$
(
'.wrapper-xblock .edit-button'
);
// The container should have rendered six mock xblocks
expect
(
editButtons
.
length
).
toBe
(
6
);
...
...
@@ -258,7 +258,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'can save changes to settings'
,
function
()
{
var
editButtons
,
modal
,
mockUpdatedXBlockHtml
;
mockUpdatedXBlockHtml
=
readFixtures
(
'mock/mock-updated-xblock.underscore'
);
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
editButtons
=
containerPage
.
$
(
'.wrapper-xblock .edit-button'
);
// The container should have rendered six mock xblocks
expect
(
editButtons
.
length
).
toBe
(
6
);
...
...
@@ -346,24 +346,24 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it
(
"can delete the first xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
deleteComponentWithSuccess
(
0
);
});
it
(
"can delete a middle xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
deleteComponentWithSuccess
(
1
);
});
it
(
"can delete the last xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
deleteComponentWithSuccess
(
NUM_COMPONENTS_PER_GROUP
-
1
);
});
it
(
'does not delete when clicking No in prompt'
,
function
()
{
var
numRequests
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
numRequests
=
requests
.
length
;
// click delete on the first component but press no
...
...
@@ -378,7 +378,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'shows a notification during the delete operation'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickDelete
(
0
);
edit_helpers
.
verifyNotificationShowing
(
notificationSpy
,
/Deleting/
);
create_sinon
.
respondWithJson
(
requests
,
{});
...
...
@@ -387,7 +387,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'does not delete an xblock upon failure'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickDelete
(
0
);
edit_helpers
.
verifyNotificationShowing
(
notificationSpy
,
/Deleting/
);
create_sinon
.
respondWithError
(
requests
);
...
...
@@ -431,23 +431,23 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it
(
"can duplicate the first xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
duplicateComponentWithSuccess
(
0
);
});
it
(
"can duplicate a middle xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
duplicateComponentWithSuccess
(
1
);
});
it
(
"can duplicate the last xblock"
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
duplicateComponentWithSuccess
(
NUM_COMPONENTS_PER_GROUP
-
1
);
});
it
(
'shows a notification when duplicating'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickDuplicate
(
0
);
edit_helpers
.
verifyNotificationShowing
(
notificationSpy
,
/Duplicating/
);
create_sinon
.
respondWithJson
(
requests
,
{
"locator"
:
"new_item"
});
...
...
@@ -456,7 +456,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'does not duplicate an xblock upon failure'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
refreshXBlockSpies
=
spyOn
(
containerPage
,
"refreshXBlock"
);
clickDuplicate
(
0
);
edit_helpers
.
verifyNotificationShowing
(
notificationSpy
,
/Duplicating/
);
...
...
@@ -475,7 +475,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it
(
'sends the correct JSON to the server'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickNewComponent
(
0
);
edit_helpers
.
verifyXBlockRequest
(
requests
,
{
"category"
:
"discussion"
,
...
...
@@ -486,7 +486,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'shows a notification while creating'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickNewComponent
(
0
);
edit_helpers
.
verifyNotificationShowing
(
notificationSpy
,
/Adding/
);
create_sinon
.
respondWithJson
(
requests
,
{
});
...
...
@@ -495,7 +495,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'does not insert component upon failure'
,
function
()
{
var
requestCount
;
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
clickNewComponent
(
0
);
requestCount
=
requests
.
length
;
create_sinon
.
respondWithError
(
requests
);
...
...
@@ -514,7 +514,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
verifyCreateHtmlComponent
=
function
(
test
,
templateIndex
,
expectedRequest
)
{
var
xblockCount
;
renderContainerPage
(
mockContainerXBlockHtml
,
test
);
renderContainerPage
(
test
,
mockContainerXBlockHtml
);
showTemplatePicker
();
xblockCount
=
containerPage
.
$
(
'.studio-xblock-wrapper'
).
length
;
containerPage
.
$
(
'.new-component-html a'
)[
templateIndex
].
click
();
...
...
cms/static/js/spec/views/pages/container_subviews_spec.js
View file @
d447c075
define
([
"jquery"
,
"underscore"
,
"underscore.string"
,
"js/spec_helpers/create_sinon"
,
"js/spec_helpers/edit_helpers"
,
"js/views/feedback_prompt"
,
"js/views/pages/container"
,
"js/views/pages/container_subviews"
,
"js/models/xblock_info"
],
"js/views/feedback_prompt"
,
"js/views/pages/container"
,
"js/views/pages/container_subviews"
,
"js/models/xblock_info"
],
function
(
$
,
_
,
str
,
create_sinon
,
edit_helpers
,
Prompt
,
ContainerPage
,
ContainerSubviews
,
XBlockInfo
)
{
describe
(
"Container Subviews"
,
function
()
{
var
model
,
containerPage
,
requests
,
renderContainerPage
,
respondWithHtml
,
respondWithJson
,
fetch
,
disabledCss
=
"is-disabled"
,
var
model
,
containerPage
,
requests
,
createContainerPage
,
renderContainerPage
,
respondWithHtml
,
respondWithJson
,
fetch
,
disabledCss
=
"is-disabled"
,
defaultXBlockInfo
,
createXBlockInfo
,
mockContainerPage
=
readFixtures
(
'mock/mock-container-page.underscore'
),
mockContainerXBlockHtml
=
readFixtures
(
'mock/mock-empty-container-xblock.underscore'
);
...
...
@@ -14,27 +15,39 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
edit_helpers
.
installTemplate
(
'publish-xblock'
);
edit_helpers
.
installTemplate
(
'publish-history'
);
edit_helpers
.
installTemplate
(
'unit-outline'
);
edit_helpers
.
installTemplate
(
'container-message'
);
appendSetFixtures
(
mockContainerPage
);
});
model
=
new
XBlockInfo
({
id
:
'locator-container'
,
display_name
:
'Test Container'
,
category
:
'vertical'
,
published
:
false
,
has_changes
:
false
},
{
parse
:
true
});
defaultXBlockInfo
=
{
id
:
'locator-container'
,
display_name
:
'Test Container'
,
category
:
'vertical'
,
published
:
false
,
has_changes
:
false
,
edited_on
:
"Jul 02, 2014 at 14:20 UTC"
,
edited_by
:
"joe"
,
published_on
:
"Jul 01, 2014 at 12:45 UTC"
,
published_by
:
"amako"
,
visible_to_staff_only
:
false
,
currently_visible_to_students
:
false
};
createXBlockInfo
=
function
(
options
)
{
return
_
.
extend
(
_
.
extend
({},
defaultXBlockInfo
),
options
||
{});
};
createContainerPage
=
function
(
test
,
options
)
{
requests
=
create_sinon
.
requests
(
test
);
model
=
new
XBlockInfo
(
createXBlockInfo
(
options
),
{
parse
:
true
});
containerPage
=
new
ContainerPage
({
model
:
model
,
templates
:
edit_helpers
.
mockComponentTemplates
,
el
:
$
(
'#content'
),
isUnitPage
:
true
});
}
)
;
};
renderContainerPage
=
function
(
html
,
that
)
{
requests
=
create_sinon
.
requests
(
that
);
renderContainerPage
=
function
(
test
,
html
,
options
)
{
createContainerPage
(
test
,
options
);
containerPage
.
render
();
respondWithHtml
(
html
);
};
...
...
@@ -57,6 +70,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
fetch
=
function
(
json
)
{
json
=
createXBlockInfo
(
json
);
model
.
fetch
();
respondWithJson
(
json
);
};
...
...
@@ -66,30 +80,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
previewCss
=
'.button-preview'
;
it
(
'renders correctly for private unit'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
expect
(
containerPage
.
$
(
viewPublishedCss
)).
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
previewCss
)).
not
.
toHaveClass
(
disabledCss
);
});
it
(
'updates when published attribute changes'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
true
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
});
expect
(
containerPage
.
$
(
viewPublishedCss
)).
not
.
toHaveClass
(
disabledCss
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
false
});
fetch
({
"published"
:
false
});
expect
(
containerPage
.
$
(
viewPublishedCss
)).
toHaveClass
(
disabledCss
);
});
it
(
'updates when has_changes attribute changes'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"
has_changes"
:
true
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"has_changes"
:
true
});
expect
(
containerPage
.
$
(
previewCss
)).
not
.
toHaveClass
(
disabledCss
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
true
,
"has_changes"
:
false
});
fetch
({
"published"
:
true
,
"has_changes"
:
false
});
expect
(
containerPage
.
$
(
previewCss
)).
toHaveClass
(
disabledCss
);
// If published is false, preview is always enabled.
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
false
,
"has_changes"
:
false
});
fetch
({
"published"
:
false
,
"has_changes"
:
false
});
expect
(
containerPage
.
$
(
previewCss
)).
not
.
toHaveClass
(
disabledCss
);
});
});
...
...
@@ -97,21 +111,21 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe
(
"Publisher"
,
function
()
{
var
headerCss
=
'.pub-status'
,
bitPublishingCss
=
"div.bit-publishing"
,
publishedBit
=
"published"
,
draftBit
=
"draft"
,
publishedBit
=
"is-published"
,
draftBit
=
"is-draft"
,
staffOnlyBit
=
"is-staff-only"
,
publishButtonCss
=
".action-publish"
,
discardChangesButtonCss
=
".action-discard"
,
lastDraftCss
=
".wrapper-last-draft"
,
releaseDateTitleCss
=
".wrapper-release .title"
,
releaseDateContentCss
=
".wrapper-release .copy"
,
lastRequest
,
promptSpies
,
sendDiscardChangesToServer
;
lastRequest
=
function
()
{
return
requests
[
requests
.
length
-
1
];
};
promptSpies
,
sendDiscardChangesToServer
;
sendDiscardChangesToServer
=
function
(
test
)
{
sendDiscardChangesToServer
=
function
()
{
// Helper function to do the discard operation, up until the server response.
renderContainerPage
(
mockContainerXBlockHtml
,
test
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"has_changes"
:
true
});
containerPage
.
render
();
respondWithHtml
(
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"has_changes"
:
true
});
expect
(
containerPage
.
$
(
discardChangesButtonCss
)).
not
.
toHaveClass
(
'is-disabled'
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
draftBit
);
// Click discard changes
...
...
@@ -132,30 +146,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'renders correctly with private content'
,
function
()
{
var
verifyPrivateState
=
function
(){
// State is the same regardless of "has_changes" value.
expect
(
containerPage
.
$
(
headerCss
).
text
()).
toContain
(
'Draft (Unpublished changes)'
);
var
verifyPrivateState
=
function
()
{
expect
(
containerPage
.
$
(
headerCss
).
text
()).
toContain
(
'Draft (Never published)'
);
expect
(
containerPage
.
$
(
publishButtonCss
)).
not
.
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
discardChangesButtonCss
)).
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
draftBit
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
draftBit
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
publishedBit
);
};
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
false
,
"has_changes"
:
false
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
false
,
"has_changes"
:
false
});
verifyPrivateState
();
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
false
,
"has_changes"
:
true
});
fetch
({
"published"
:
false
,
"has_changes"
:
true
});
verifyPrivateState
();
});
it
(
'renders correctly with public content'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
true
,
"has_changes"
:
false
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"has_changes"
:
false
});
expect
(
containerPage
.
$
(
headerCss
).
text
()).
toContain
(
'Published'
);
expect
(
containerPage
.
$
(
publishButtonCss
)).
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
discardChangesButtonCss
)).
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
publishedBit
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
true
,
"has_changes"
:
true
});
fetch
({
"published"
:
true
,
"has_changes"
:
true
});
expect
(
containerPage
.
$
(
headerCss
).
text
()).
toContain
(
'Draft (Unpublished changes)'
);
expect
(
containerPage
.
$
(
publishButtonCss
)).
not
.
toHaveClass
(
disabledCss
);
expect
(
containerPage
.
$
(
discardChangesButtonCss
)).
not
.
toHaveClass
(
disabledCss
);
...
...
@@ -164,9 +178,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'can publish private content'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
false
,
"has_changes"
:
false
});
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
draftBit
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
false
,
"has_changes"
:
false
});
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
draftBit
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
publishedBit
);
// Click publish
containerPage
.
$
(
publishButtonCss
).
click
();
...
...
@@ -191,9 +206,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'can does not fetch if publish fails'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"published"
:
false
,
"has_changes
"
:
false
});
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
draft
Bit
);
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"
published
"
:
false
});
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
published
Bit
);
// Click publish
containerPage
.
$
(
publishButtonCss
).
click
();
...
...
@@ -205,17 +220,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
expect
(
requests
.
length
).
toEqual
(
numRequests
);
// Verify still in draft state.
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
draft
Bit
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
published
Bit
);
// Verify that the "published" value has been cleared out of the model.
expect
(
containerPage
.
model
.
get
(
"publish"
)).
toBeNull
();
});
it
(
'can discard changes'
,
function
()
{
var
notificationSpy
=
edit_helpers
.
createNotificationSpy
(),
renderPageSpy
=
spyOn
(
containerPage
.
xblockPublisher
,
'renderPage'
).
andCallThrough
(),
numRequests
;
var
notificationSpy
,
renderPageSpy
,
numRequests
;
createContainerPage
(
this
);
notificationSpy
=
edit_helpers
.
createNotificationSpy
();
renderPageSpy
=
spyOn
(
containerPage
.
xblockPublisher
,
'renderPage'
).
andCallThrough
();
sendDiscardChangesToServer
(
this
);
sendDiscardChangesToServer
();
numRequests
=
requests
.
length
;
// Respond with success.
...
...
@@ -230,10 +246,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'does not fetch if discard changes fails'
,
function
()
{
var
renderPageSpy
=
spyOn
(
containerPage
.
xblockPublisher
,
'renderPage'
).
andCallThrough
(),
numRequests
;
var
renderPageSpy
,
numRequests
;
createContainerPage
(
this
);
renderPageSpy
=
spyOn
(
containerPage
.
xblockPublisher
,
'renderPage'
).
andCallThrough
();
sendDiscardChangesToServer
(
this
);
sendDiscardChangesToServer
();
numRequests
=
requests
.
length
;
// Respond with failure
...
...
@@ -246,8 +263,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'does not discard changes on cancel'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"
id"
:
"locator-container"
,
"
published"
:
true
,
"has_changes"
:
true
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"has_changes"
:
true
});
var
numRequests
=
requests
.
length
;
// Click discard changes
...
...
@@ -262,84 +279,213 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it
(
'renders the last published date and user when there are no changes'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"has_changes"
:
false
,
"edited_on"
:
"Jun 30, 2014 at 14:20 UTC"
,
"edited_by"
:
"joe"
,
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
expect
(
containerPage
.
$
(
lastDraftCss
).
text
()).
toContain
(
"Last published Jul 01, 2014 at 12:45 UTC by amako"
);
});
it
(
'renders the last saved date and user when there are changes'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"has_changes"
:
true
,
"edited_on"
:
"Jul 02, 2014 at 14:20 UTC"
,
"edited_by"
:
"joe"
,
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"has_changes"
:
true
,
"edited_on"
:
"Jul 02, 2014 at 14:20 UTC"
,
"edited_by"
:
"joe"
});
expect
(
containerPage
.
$
(
lastDraftCss
).
text
()).
toContain
(
"Draft saved on Jul 02, 2014 at 14:20 UTC by joe"
);
});
it
(
'renders the release date correctly when unreleased'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"released_to_students"
:
false
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Scheduled:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
describe
(
"Release Date"
,
function
()
{
it
(
'renders correctly when unreleased'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"released_to_students"
:
false
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Scheduled:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
});
it
(
'renders correctly when released'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"released_to_students"
:
true
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Released:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
});
it
(
'renders correctly when the release date is not set'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"released_to_students"
:
false
,
"release_date"
:
null
,
"release_date_from"
:
null
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Release:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
"Unscheduled"
);
});
it
(
'renders correctly when the unit is not published'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
false
,
"released_to_students"
:
true
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
// Force a render because none of the fetched fields will trigger a render
containerPage
.
xblockPublisher
.
render
();
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Release:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
});
});
it
(
'renders the release date correctly when released'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"released_to_students"
:
true
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Released:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
});
describe
(
"Content Visibility"
,
function
()
{
var
requestStaffOnly
,
verifyStaffOnly
,
promptSpy
;
requestStaffOnly
=
function
(
isStaffOnly
)
{
containerPage
.
$
(
'.action-staff-lock'
).
click
();
// If removing the staff lock, click 'Yes' to confirm
if
(
!
isStaffOnly
)
{
edit_helpers
.
confirmPrompt
(
promptSpy
);
}
create_sinon
.
expectJsonRequest
(
requests
,
'POST'
,
'/xblock/locator-container'
,
{
publish
:
'republish'
,
metadata
:
{
visible_to_staff_only
:
isStaffOnly
}
});
create_sinon
.
respondWithJson
(
requests
,
{
data
:
null
,
id
:
"locator-container"
,
metadata
:
{
visible_to_staff_only
:
isStaffOnly
}
});
create_sinon
.
expectJsonRequest
(
requests
,
'GET'
,
'/xblock/locator-container'
);
create_sinon
.
respondWithJson
(
requests
,
createXBlockInfo
({
published
:
containerPage
.
model
.
get
(
'published'
),
visible_to_staff_only
:
isStaffOnly
}));
};
it
(
'renders the release date correctly when the release date is not set'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"released_to_students"
:
false
,
"release_date"
:
null
,
"release_date_from"
:
null
});
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Release:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
"Unscheduled"
);
});
verifyStaffOnly
=
function
(
isStaffOnly
)
{
if
(
isStaffOnly
)
{
expect
(
containerPage
.
$
(
'.action-staff-lock i'
)).
toHaveClass
(
'icon-check'
);
expect
(
containerPage
.
$
(
'.wrapper-visibility .copy'
).
text
()).
toBe
(
'Staff Only'
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
staffOnlyBit
);
}
else
{
expect
(
containerPage
.
$
(
'.action-staff-lock i'
)).
toHaveClass
(
'icon-check-empty'
);
expect
(
containerPage
.
$
(
'.wrapper-visibility .copy'
).
text
()).
toBe
(
'Staff and Students'
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
staffOnlyBit
);
}
};
it
(
'renders the release date correctly when the unit is not published'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
false
,
"released_to_students"
:
true
,
"release_date"
:
"Jul 02, 2014 at 14:20 UTC"
,
"release_date_from"
:
'Section "Week 1"'
});
// Force a render because none of the fetched fields will trigger a render
containerPage
.
xblockPublisher
.
render
();
expect
(
containerPage
.
$
(
releaseDateTitleCss
).
text
()).
toContain
(
"Release:"
);
expect
(
containerPage
.
$
(
releaseDateContentCss
).
text
()).
toContain
(
'Jul 02, 2014 at 14:20 UTC with Section "Week 1"'
);
it
(
"is initially shown to all"
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
verifyStaffOnly
(
false
);
});
it
(
"can be set to staff only"
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
containerPage
.
$
(
'.action-staff-lock'
).
click
();
requestStaffOnly
(
true
);
verifyStaffOnly
(
true
);
});
it
(
"can remove staff only setting"
,
function
()
{
promptSpy
=
edit_helpers
.
createPromptSpy
();
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
requestStaffOnly
(
true
);
requestStaffOnly
(
false
);
verifyStaffOnly
(
false
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
not
.
toHaveClass
(
publishedBit
);
});
it
(
"can remove staff only setting from published unit"
,
function
()
{
promptSpy
=
edit_helpers
.
createPromptSpy
();
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
published
:
true
});
requestStaffOnly
(
true
);
requestStaffOnly
(
false
);
verifyStaffOnly
(
false
);
expect
(
containerPage
.
$
(
bitPublishingCss
)).
toHaveClass
(
publishedBit
);
});
it
(
"does not refresh if removing staff only is canceled"
,
function
()
{
var
requestCount
;
promptSpy
=
edit_helpers
.
createPromptSpy
();
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
requestStaffOnly
(
true
);
requestCount
=
requests
.
length
;
containerPage
.
$
(
'.action-staff-lock'
).
click
();
edit_helpers
.
confirmPrompt
(
promptSpy
,
true
);
// Click 'No' to cancel
expect
(
requests
.
length
).
toBe
(
requestCount
);
verifyStaffOnly
(
true
);
});
it
(
"does not refresh when failing to set staff only"
,
function
()
{
var
requestCount
;
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
containerPage
.
$
(
'.lock-checkbox'
).
click
();
requestCount
=
requests
.
length
;
create_sinon
.
respondWithError
(
requests
);
expect
(
requests
.
length
).
toBe
(
requestCount
);
verifyStaffOnly
(
false
);
});
});
});
describe
(
"PublishHistory"
,
function
()
{
var
lastPublishCss
=
".wrapper-last-publish"
;
it
(
'renders the last published date and user when the block is published'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
it
(
'renders the last published date and user when the block is published'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
expect
(
containerPage
.
$
(
lastPublishCss
).
text
()).
toContain
(
"Last published Jul 01, 2014 at 12:45 UTC by amako"
);
});
it
(
'renders never published when the block is unpublished'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
false
,
"published_on"
:
"Jul 01, 2014 at 12:45 UTC"
,
"published_by"
:
"amako"
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
false
});
expect
(
containerPage
.
$
(
lastPublishCss
).
text
()).
toContain
(
"Never published"
);
});
it
(
'renders correctly when the block is published without publish info'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
fetch
({
"id"
:
"locator-container"
,
"published"
:
true
,
"published_on"
:
null
,
"published_by"
:
null
});
renderContainerPage
(
this
,
mockContainerXBlockHtml
);
fetch
({
"published"
:
true
,
"published_on"
:
null
,
"published_by"
:
null
});
expect
(
containerPage
.
$
(
lastPublishCss
).
text
()).
toContain
(
"Previously published"
);
});
});
describe
(
"Message Area"
,
function
()
{
var
messageSelector
=
'.container-message .warning'
,
warningMessage
=
'This content is live for students. Edit with caution.'
;
it
(
'is empty for a unit that is not currently visible to students'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
currently_visible_to_students
:
false
});
expect
(
containerPage
.
$
(
messageSelector
).
text
().
trim
()).
toBe
(
''
);
});
it
(
'shows a message for a unit that is currently visible to students'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
currently_visible_to_students
:
true
});
expect
(
containerPage
.
$
(
messageSelector
).
text
().
trim
()).
toBe
(
warningMessage
);
});
it
(
'hides the message when the unit is hidden from students'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
currently_visible_to_students
:
true
});
fetch
({
currently_visible_to_students
:
false
});
expect
(
containerPage
.
$
(
messageSelector
).
text
().
trim
()).
toBe
(
''
);
});
it
(
'shows a message when a unit is made visible'
,
function
()
{
renderContainerPage
(
this
,
mockContainerXBlockHtml
,
{
currently_visible_to_students
:
false
});
fetch
({
currently_visible_to_students
:
true
});
expect
(
containerPage
.
$
(
messageSelector
).
text
().
trim
()).
toBe
(
warningMessage
);
});
});
});
});
cms/static/js/views/pages/container.js
View file @
d447c075
...
...
@@ -29,6 +29,11 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
model
:
this
.
model
,
view
:
this
.
view
});
this
.
messageView
=
new
ContainerSubviews
.
MessageView
({
el
:
this
.
$
(
'.container-message'
),
model
:
this
.
model
});
this
.
messageView
.
render
();
this
.
isUnitPage
=
this
.
options
.
isUnitPage
;
if
(
this
.
isUnitPage
)
{
this
.
xblockPublisher
=
new
ContainerSubviews
.
Publisher
({
...
...
cms/static/js/views/pages/container_subviews.js
View file @
d447c075
...
...
@@ -10,7 +10,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
* A view that calls render when "has_changes" or "published" values in XBlockInfo have changed
* after a server sync operation.
*/
var
UnitStateListenerView
=
BaseView
.
extend
({
var
ContainerStateListenerView
=
BaseView
.
extend
({
// takes XBlockInfo as a model
initialize
:
function
()
{
...
...
@@ -18,18 +18,43 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync
:
function
(
model
)
{
if
(
ViewUtils
.
hasChangedAttributes
(
model
,
[
'has_changes'
,
'published'
]
))
{
if
(
this
.
shouldRefresh
(
model
))
{
this
.
render
();
}
},
shouldRefresh
:
function
(
model
)
{
return
false
;
},
render
:
function
()
{}
});
var
MessageView
=
ContainerStateListenerView
.
extend
({
initialize
:
function
()
{
ContainerStateListenerView
.
prototype
.
initialize
.
call
(
this
);
this
.
template
=
this
.
loadTemplate
(
'container-message'
);
},
shouldRefresh
:
function
(
model
)
{
return
ViewUtils
.
hasChangedAttributes
(
model
,
[
'currently_visible_to_students'
]);
},
render
:
function
()
{
this
.
$el
.
html
(
this
.
template
({
currentlyVisibleToStudents
:
this
.
model
.
get
(
'currently_visible_to_students'
)
}));
return
this
;
}
});
/**
* A controller for updating the "View Live" and "Preview" buttons.
*/
var
PreviewActionController
=
UnitStateListenerView
.
extend
({
var
PreviewActionController
=
ContainerStateListenerView
.
extend
({
shouldRefresh
:
function
(
model
)
{
return
ViewUtils
.
hasChangedAttributes
(
model
,
[
'has_changes'
,
'published'
]);
},
render
:
function
()
{
var
previewAction
=
this
.
$el
.
find
(
'.button-preview'
),
...
...
@@ -59,7 +84,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var
Publisher
=
BaseView
.
extend
({
events
:
{
'click .action-publish'
:
'publish'
,
'click .action-discard'
:
'discardChanges'
'click .action-discard'
:
'discardChanges'
,
'click .action-staff-lock'
:
'toggleStaffLock'
},
// takes XBlockInfo as a model
...
...
@@ -72,22 +98,25 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync
:
function
(
model
)
{
if
(
ViewUtils
.
hasChangedAttributes
(
model
,
[
'has_changes'
,
'published'
,
'edited_on'
,
'edited_by'
]))
{
if
(
ViewUtils
.
hasChangedAttributes
(
model
,
[
'has_changes'
,
'published'
,
'edited_on'
,
'edited_by'
,
'visible_to_staff_only'
]))
{
this
.
render
();
}
},
render
:
function
()
{
this
.
$el
.
html
(
this
.
template
({
has
_c
hanges
:
this
.
model
.
get
(
'has_changes'
),
has
C
hanges
:
this
.
model
.
get
(
'has_changes'
),
published
:
this
.
model
.
get
(
'published'
),
edited_on
:
this
.
model
.
get
(
'edited_on'
),
edited_by
:
this
.
model
.
get
(
'edited_by'
),
published_on
:
this
.
model
.
get
(
'published_on'
),
published_by
:
this
.
model
.
get
(
'published_by'
),
released_to_students
:
this
.
model
.
get
(
'released_to_students'
),
release_date
:
this
.
model
.
get
(
'release_date'
),
release_date_from
:
this
.
model
.
get
(
'release_date_from'
)
editedOn
:
this
.
model
.
get
(
'edited_on'
),
editedBy
:
this
.
model
.
get
(
'edited_by'
),
publishedOn
:
this
.
model
.
get
(
'published_on'
),
publishedBy
:
this
.
model
.
get
(
'published_by'
),
releasedToStudents
:
this
.
model
.
get
(
'released_to_students'
),
releaseDate
:
this
.
model
.
get
(
'release_date'
),
releaseDateFrom
:
this
.
model
.
get
(
'release_date_from'
),
visibleToStaffOnly
:
this
.
model
.
get
(
'visible_to_staff_only'
)
}));
return
this
;
...
...
@@ -127,10 +156,60 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
});
}
);
},
toggleStaffLock
:
function
(
e
)
{
var
xblockInfo
=
this
.
model
,
self
=
this
,
enableStaffLock
,
saveAndPublishStaffLock
,
revertCheckBox
;
if
(
e
&&
e
.
preventDefault
)
{
e
.
preventDefault
();
}
enableStaffLock
=
!
xblockInfo
.
get
(
'visible_to_staff_only'
);
revertCheckBox
=
function
()
{
self
.
checkStaffLock
(
!
enableStaffLock
);
};
saveAndPublishStaffLock
=
function
()
{
return
xblockInfo
.
save
({
publish
:
'republish'
,
metadata
:
{
visible_to_staff_only
:
enableStaffLock
}},
{
patch
:
true
}
).
always
(
function
()
{
xblockInfo
.
set
(
"publish"
,
null
);
}).
done
(
function
()
{
xblockInfo
.
fetch
();
}).
fail
(
function
()
{
revertCheckBox
();
});
};
this
.
checkStaffLock
(
enableStaffLock
);
if
(
enableStaffLock
)
{
ViewUtils
.
runOperationShowingMessage
(
gettext
(
'Setting Staff Lock…'
),
_
.
bind
(
saveAndPublishStaffLock
,
self
));
}
else
{
ViewUtils
.
confirmThenRunOperation
(
gettext
(
"Remove Staff Lock"
),
gettext
(
"Are you sure you want to remove the staff lock? Once you publish this unit, it will be released to students on the release date."
),
gettext
(
"Remove Staff Lock"
),
function
()
{
ViewUtils
.
runOperationShowingMessage
(
gettext
(
'Removing Staff Lock…'
),
_
.
bind
(
saveAndPublishStaffLock
,
self
));
},
function
()
{
// On cancel, revert the check in the check box
revertCheckBox
();
}
);
}
},
checkStaffLock
:
function
(
check
)
{
this
.
$
(
'.action-staff-lock i'
).
removeClass
(
'icon-check icon-check-empty'
);
this
.
$
(
'.action-staff-lock i'
).
addClass
(
check
?
'icon-check'
:
'icon-check-empty'
);
}
});
/**
* PublishHistory displays when and by whom the xblock was last published, if it ever was.
*/
...
...
@@ -161,6 +240,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
});
return
{
'MessageView'
:
MessageView
,
'PreviewActionController'
:
PreviewActionController
,
'Publisher'
:
Publisher
,
'PublishHistory'
:
PublishHistory
...
...
cms/static/js/views/utils/view_utils.js
View file @
d447c075
...
...
@@ -33,7 +33,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
/**
* Confirms with the user whether to run an operation or not, and then runs it if desired.
*/
confirmThenRunOperation
=
function
(
title
,
message
,
actionLabel
,
operation
)
{
confirmThenRunOperation
=
function
(
title
,
message
,
actionLabel
,
operation
,
onCancelCallback
)
{
return
new
PromptView
.
Warning
({
title
:
title
,
message
:
message
,
...
...
@@ -48,6 +48,9 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
secondary
:
{
text
:
gettext
(
'Cancel'
),
click
:
function
(
prompt
)
{
if
(
onCancelCallback
)
{
onCancelCallback
();
}
return
prompt
.
hide
();
}
}
...
...
cms/static/sass/_developer.scss
View file @
d447c075
...
...
@@ -11,8 +11,7 @@
//.wrapper-xblock-header {
.view-outline
,
.view-container
{
.view-outline
{
.add-xblock-component
{
text-align
:
center
;
...
...
cms/static/sass/views/_container.scss
View file @
d447c075
...
...
@@ -111,11 +111,15 @@
&
.staff-only
,
&
.is-staff-only
{
@extend
%bar-module-black
;
&
.is-scheduled
.wrapper-release
.copy
{
text-decoration
:
line-through
;
}
}
.bar-mod-content
{
border
:
0
;
padding
:
(
$baseline
/
2
)
(
$baseline
*.
75
)
(
$baseline
*.
75
)
(
$baseline
*.
75
);
padding
:
(
$baseline
/
2
)
(
$baseline
*.
75
)
(
$baseline
/
4
)
(
$baseline
*.
75
);
.title
{
margin-bottom
:
(
$baseline
/
10
);
...
...
@@ -123,7 +127,6 @@
}
.wrapper-last-draft
{
padding
:
(
$baseline
*.
75
)
(
$baseline
*.
75
)
(
$baseline
/
4
)
(
$baseline
*.
75
);
.date
,
.user
{
...
...
@@ -145,9 +148,9 @@
font-weight
:
600
;
}
.action-inline
[
class
^=
"icon-"
]
{
margin
:
0
(
$baseline
/
4
);
[
class
^=
"icon-"
]
{
margin
-left
:
(
$baseline
/
4
);
color
:
$gray-d1
;
}
}
...
...
@@ -215,107 +218,7 @@
}
.wrapper-unit-tree-location
{
.draggable-drop-indicator
{
display
:
none
;
}
// need to explicitly set this since the html structure is different than the others
.section-name
:hover
{
background
:
$blue-l5
;
color
:
$blue
;
}
.subsection
,
.courseware-unit
{
margin
:
(
$baseline
/
4
)
0
0
(
$baseline
*.
75
);
}
.courseware-unit
.section-item
{
background-color
:
transparent
;
}
.section-item
{
@include
transition
(
background
$tmg-avg
ease-in-out
0
);
@include
box-sizing
(
border-box
);
@extend
%t-copy-sub2
;
width
:
100%
;
display
:
inline-block
;
vertical-align
:
top
;
overflow
:
hidden
;
padding
:
6px
8px
8px
16px
;
background
:
$gray-l5
;
white-space
:
nowrap
;
text-overflow
:
ellipsis
;
color
:
$gray
;
&
:hover
{
background
:
$blue-l5
;
color
:
$blue
;
}
&
.editing
{
background-color
:
$orange-l3
;
}
// TODO: update these once we have different pub states
.public-item
{
color
:
$black
;
}
.private-item
{
color
:
$gray-l1
;
}
.draft-item
{
color
:
$yellow-d1
;
}
.public-item
:hover
,
.private-item
:hover
,
.draft-item
:hover
{
color
:
$blue
;
}
.draft-item
:after
,
.public-item
:after
,
.private-item
:after
{
@include
font-size
(
9
);
margin-left
:
3px
;
font-weight
:
600
;
text-transform
:
uppercase
;
}
.draft-item
:after
{
content
:
"- draft"
;
}
.private-item
:after
{
content
:
"- private"
;
}
}
.subsection
>
.section-item
:hover
{
background-color
:
$gray-l5
;
color
:
inherit
;
}
.new-unit-item
{
@extend
%ui-btn-flat-outline
;
@extend
%t-action4
;
width
:
90%
;
margin
:
0
0
(
$baseline
/
2
)
(
$baseline
/
4
);
border
:
1px
solid
transparent
;
padding
:
(
$baseline
/
4
)
(
$baseline
/
2
);
font-weight
:
normal
;
color
:
$gray-l2
;
text-align
:
left
;
&
:hover
{
box-shadow
:
none
;
background-image
:
none
;
}
}
// tree location-specific styles should go here
}
}
}
...
...
cms/templates/container.html
View file @
d447c075
...
...
@@ -25,7 +25,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
"
editor-mode-button
",
"
upload-dialog
",
"
image-modal
",
"
add-xblock-component
",
"
add-xblock-component-button
",
"
add-xblock-component-menu
",
"
add-xblock-component-menu-problem
",
"
xblock-string-field-editor
",
"
publish-xblock
",
"
publish-history
",
"
unit-outline
"]
"
unit-outline
"
,
"
container-message
"
]
%
>
<
%
block
name=
"header_extras"
>
% for template_name in templates:
...
...
@@ -116,16 +116,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
<section
class=
"content-area"
>
<article
class=
"content-primary"
>
% if is_visible_to_students:
<div
class=
"container-message wrapper-message"
>
<div
class=
"message has-warnings"
>
<p
class=
"warning"
>
<i
class=
"icon-warning-sign"
></i>
${_("This content is live for students. Edit with caution.")}
</p>
</div>
</div>
% endif
<div
class=
"container-message wrapper-message"
></div>
<section
class=
"wrapper-xblock level-page is-hidden studio-xblock-wrapper"
data-locator=
"${xblock_locator}"
data-course-key=
"${xblock_locator.course_key}"
>
</section>
<div
class=
"ui-loading"
>
...
...
cms/templates/js/container-message.underscore
0 → 100644
View file @
d447c075
<% if (currentlyVisibleToStudents) { %>
<div class="message has-warnings">
<p class="warning">
<i class="icon-warning-sign"></i>
<%= gettext("This content is live for students. Edit with caution.") %>
</p>
</div>
<% } %>
cms/templates/js/course-outline.underscore
View file @
d447c075
...
...
@@ -14,8 +14,8 @@
<% if (xblockInfo.get('category') === 'vertical') { %>
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
<% } else { %>
<span class="wrapper-xblock-field is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="xblock-field-value"><%= xblockInfo.get('display_name') %></span>
<span class="wrapper-xblock-field i
ncontext-editor i
s-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="xblock-field-value
incontext-editor-value
"><%= xblockInfo.get('display_name') %></span>
</span>
<% } %>
</h3>
...
...
cms/templates/js/mock/mock-container-page.underscore
View file @
d447c075
...
...
@@ -43,6 +43,7 @@
<section class="content-area">
<article class="content-primary window">
<div class="container-message wrapper-message"></div>
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container">
</section>
<div class="ui-loading is-hidden">
...
...
cms/templates/js/publish-xblock.underscore
View file @
d447c075
<div class="bit-publishing <% if (published && !has_changes) { %>published<% } else { %>draft<%} %>">
<%
var publishClasses = "";
var title = gettext("Draft (Never published)");
if (published) {
if (published && hasChanges) {
publishClasses = publishClasses + " is-draft";
title = gettext("Draft (Unpublished changes)");
} else {
publishClasses = publishClasses + " is-published";
title = gettext("Published");
}
}
if (releaseDate) {
publishClasses = publishClasses + " is-scheduled";
}
if (visibleToStaffOnly) {
publishClasses = publishClasses + " is-staff-only";
title = gettext("Unpublished (Staff only)");
}
%>
<div class="bit-publishing <%= publishClasses %>">
<h3 class="bar-mod-title pub-status"><span class="sr"><%= gettext("Publishing Status") %></span>
<% if (published && !has_changes) { %>
<%= gettext("Published") %>
<% } else { %>
<%= gettext("Draft (Unpublished changes)") %>
<% } %>
<%= title %>
</h3>
<div class="wrapper-last-draft bar-mod-content">
<p class="copy meta">
<% if (has
_changes && edited_on && edited_b
y) {
<% if (has
Changes && editedOn && editedB
y) {
var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %>
<%= interpolate(message, {
last_saved_date: '<span class="date">' + edited
_o
n + '</span>',
edit_username: '<span class="user">' + edited
_b
y + '</span>' }, true) %>
<% } else if (published
_on && published_b
y) {
last_saved_date: '<span class="date">' + edited
O
n + '</span>',
edit_username: '<span class="user">' + edited
B
y + '</span>' }, true) %>
<% } else if (published
On && publishedB
y) {
var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %>
<%= interpolate(message, {
last_published_date: '<span class="date">' + published
_o
n + '</span>',
publish_username: '<span class="user">' + published
_b
y + '</span>' }, true) %>
last_published_date: '<span class="date">' + published
O
n + '</span>',
publish_username: '<span class="user">' + published
B
y + '</span>' }, true) %>
<% } else { %>
<%= gettext("Previously published") %>
<% } %>
</p>
</div>
<!--TODO this needs strikeout styles once staff lock exists-->
<div class="wrapper-release bar-mod-content">
<h5 class="title">
<% if (published && release
_d
ate) {
if (released
_to_s
tudents) { %>
<% if (published && release
D
ate) {
if (released
ToS
tudents) { %>
<%= gettext("Released:") %>
<% } else { %>
<%= gettext("Scheduled:") %>
...
...
@@ -39,37 +54,45 @@
<% } %>
</h5>
<p class="copy">
<% if (release
_d
ate) { %>
<% if (release
D
ate) { %>
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
<%= interpolate(message, {
release_date: '<span class="release-date">' + release
_d
ate + '</span>',
section_or_subsection: '<span class="release-with">' + release
_date_f
rom + '</span>' }, true) %>
release_date: '<span class="release-date">' + release
D
ate + '</span>',
section_or_subsection: '<span class="release-with">' + release
DateF
rom + '</span>' }, true) %>
<% } else { %>
<%= gettext("Unscheduled") %>
<% } %>
</p>
</div>
<!--To be added in STUD-1830-->
<!--<div class="wrapper-visibility bar-mod-content">-->
<!--<h5 class="title">Will be Visible to:</h5>-->
<!--<p class="copy">Staff and Students</p>-->
<!--<p class="action-inline">-->
<!--<a href="">-->
<!--<i class="icon-unlock is-disabled"></i> Hide from Students-->
<!--</a>-->
<!--</p>-->
<!--</div>-->
<div class="wrapper-visibility bar-mod-content">
<h5 class="title"><%= gettext("Will Be Visible To:") %></h5>
<% if (visibleToStaffOnly) { %>
<p class="copy"><%= gettext("Staff Only") %></p>
<% } else { %>
<p class="copy"><%= gettext("Staff and Students") %></p>
<% } %>
<p class="action-inline">
<a href="" class="action-staff-lock" role="button" aria-pressed="<%= visibleToStaffOnly %>">
<% if (visibleToStaffOnly) { %>
<i class="icon-check"></i>
<% } else { %>
<i class="icon-check-empty"></i>
<% } %>
<%= gettext('Hide from students') %>
</a>
</p>
</div>
<div class="wrapper-pub-actions bar-mod-actions">
<ul class="action-list">
<li class="action-item">
<a class="action-publish action-primary <% if (published && !has
_c
hanges) { %>is-disabled<% } %>"
<a class="action-publish action-primary <% if (published && !has
C
hanges) { %>is-disabled<% } %>"
href=""><%= gettext("Publish") %>
</a>
</li>
<li class="action-item">
<a class="action-discard action-secondary <% if (!published || !has
_c
hanges) { %>is-disabled<% } %>"
<a class="action-discard action-secondary <% if (!published || !has
C
hanges) { %>is-disabled<% } %>"
href=""><%= gettext("Discard Changes") %>
</a>
</li>
...
...
common/test/acceptance/pages/lms/staff_view.py
View file @
d447c075
...
...
@@ -10,16 +10,24 @@ class StaffPage(PageObject):
"""
url
=
None
STAFF_STATUS_CSS
=
'#staffstatus'
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'#staffstatus'
)
.
present
return
self
.
q
(
css
=
self
.
STAFF_STATUS_CSS
)
.
present
@property
def
staff_status
(
self
):
"""
Return the current status, either Staff view or Student view
"""
return
self
.
q
(
css
=
'#staffstatus'
)
.
text
[
0
]
return
self
.
q
(
css
=
self
.
STAFF_STATUS_CSS
)
.
text
[
0
]
def
toggle_staff_view
(
self
):
"""
Toggle between staff view and student view.
"""
self
.
q
(
css
=
self
.
STAFF_STATUS_CSS
)
.
first
.
click
()
self
.
wait_for_ajax
()
def
open_staff_debug_info
(
self
):
"""
...
...
common/test/acceptance/pages/studio/container.py
View file @
d447c075
...
...
@@ -82,6 +82,33 @@ class ContainerPage(PageObject):
return
self
.
q
(
css
=
'.pub-status'
)
.
first
.
text
[
0
]
@property
def
release_title
(
self
):
"""
Returns the title before the release date in the publishing sidebar component.
"""
return
self
.
q
(
css
=
'.wrapper-release .title'
)
.
first
.
text
[
0
]
@property
def
release_date
(
self
):
"""
Returns the release date of the unit (with ancestor inherited from), as displayed
in the publishing sidebar component.
"""
return
self
.
q
(
css
=
'.wrapper-release .copy'
)
.
first
.
text
[
0
]
@property
def
currently_visible_to_students
(
self
):
"""
Returns True if the unit is marked as currently visible to students
(meaning that a warning is being displayed).
"""
warnings
=
self
.
q
(
css
=
'.container-message .warning'
)
if
not
warnings
.
is_present
():
return
False
warning_text
=
warnings
.
first
.
text
[
0
]
return
warning_text
==
"This content is live for students. Edit with caution."
@property
def
publish_action
(
self
):
"""
Returns the link for publishing a unit.
...
...
@@ -96,6 +123,22 @@ class ContainerPage(PageObject):
self
.
q
(
css
=
'a.button.action-primary'
)
.
first
.
click
()
self
.
wait_for_ajax
()
def
toggle_staff_lock
(
self
):
"""
Toggles "hide from students" which enables or disables a staff-only lock.
Returns True if the lock is now enabled, else False.
"""
class_attribute_values
=
self
.
q
(
css
=
'a.action-staff-lock>i'
)
.
attrs
(
'class'
)
was_locked_initially
=
'icon-check'
in
class_attribute_values
if
not
was_locked_initially
:
self
.
q
(
css
=
'a.action-staff-lock'
)
.
first
.
click
()
else
:
click_css
(
self
,
'a.action-staff-lock'
,
0
,
require_notification
=
False
)
self
.
q
(
css
=
'a.button.action-primary'
)
.
first
.
click
()
self
.
wait_for_ajax
()
return
not
was_locked_initially
def
view_published_version
(
self
):
"""
Clicks "View Published Version", which will open the published version of the unit page in the LMS.
...
...
common/test/acceptance/tests/test_lms.py
View file @
d447c075
...
...
@@ -16,6 +16,7 @@ from ..pages.lms.progress import ProgressPage
from
..pages.lms.dashboard
import
DashboardPage
from
..pages.lms.video.video
import
VideoPage
from
..pages.xblock.acid
import
AcidView
from
..pages.lms.courseware
import
CoursewarePage
from
..fixtures.course
import
CourseFixture
,
XBlockFixtureDesc
,
CourseUpdateDesc
...
...
@@ -421,3 +422,87 @@ class XBlockAcidChildTest(XBlockAcidBase):
@skip
(
'This will fail until we fix support of children in pure XBlocks'
)
def
test_acid_block
(
self
):
super
(
XBlockAcidChildTest
,
self
)
.
test_acid_block
()
class
VisibleToStaffOnlyTest
(
UniqueCourseTest
):
"""
Tests that content with visible_to_staff_only set to True cannot be viewed by students.
"""
def
setUp
(
self
):
super
(
VisibleToStaffOnlyTest
,
self
)
.
setUp
()
course_fix
=
CourseFixture
(
self
.
course_info
[
'org'
],
self
.
course_info
[
'number'
],
self
.
course_info
[
'run'
],
self
.
course_info
[
'display_name'
]
)
course_fix
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
'Test Section'
)
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Subsection With Locked Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'Locked Unit'
,
metadata
=
{
'visible_to_staff_only'
:
True
})
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'Html Child in locked unit'
,
data
=
"<html>Visible only to staff</html>"
),
),
XBlockFixtureDesc
(
'vertical'
,
'Unlocked Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'Html Child in unlocked unit'
,
data
=
"<html>Visible only to all</html>"
),
)
),
XBlockFixtureDesc
(
'sequential'
,
'Unlocked Subsection'
)
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'Test Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'Html Child in visible unit'
,
data
=
"<html>Visible to all</html>"
),
)
),
XBlockFixtureDesc
(
'sequential'
,
'Locked Subsection'
,
metadata
=
{
'visible_to_staff_only'
:
True
})
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'Test Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'html'
,
'Html Child in locked subsection'
,
data
=
"<html>Visible only to staff</html>"
)
)
)
)
)
.
install
()
self
.
courseware_page
=
CoursewarePage
(
self
.
browser
,
self
.
course_id
)
self
.
course_nav
=
CourseNavPage
(
self
.
browser
)
def
test_visible_to_staff
(
self
):
"""
Scenario: All content is visible for a user marked is_staff (different from course staff)
Given some of the course content has been marked 'visible_to_staff_only'
And I am logged on with an account marked 'is_staff'
Then I can see all course content
"""
AutoAuthPage
(
self
.
browser
,
username
=
"STAFF_TESTER"
,
email
=
"johndoe_staff@example.com"
,
course_id
=
self
.
course_id
,
staff
=
True
)
.
visit
()
self
.
courseware_page
.
visit
()
self
.
assertEqual
(
3
,
len
(
self
.
course_nav
.
sections
[
'Test Section'
]))
self
.
course_nav
.
go_to_section
(
"Test Section"
,
"Subsection With Locked Unit"
)
self
.
assertEqual
([
"Html Child in locked unit"
,
"Html Child in unlocked unit"
],
self
.
course_nav
.
sequence_items
)
self
.
course_nav
.
go_to_section
(
"Test Section"
,
"Unlocked Subsection"
)
self
.
assertEqual
([
"Html Child in visible unit"
],
self
.
course_nav
.
sequence_items
)
self
.
course_nav
.
go_to_section
(
"Test Section"
,
"Locked Subsection"
)
self
.
assertEqual
([
"Html Child in locked subsection"
],
self
.
course_nav
.
sequence_items
)
def
test_visible_to_student
(
self
):
"""
Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
Given some of the course content has been marked 'visible_to_staff_only'
And I am logged on with an authorized student account
Then I can only see content without 'visible_to_staff_only' set to True
"""
AutoAuthPage
(
self
.
browser
,
username
=
"STUDENT_TESTER"
,
email
=
"johndoe_student@example.com"
,
course_id
=
self
.
course_id
,
staff
=
False
)
.
visit
()
self
.
courseware_page
.
visit
()
self
.
assertEqual
(
2
,
len
(
self
.
course_nav
.
sections
[
'Test Section'
]))
self
.
course_nav
.
go_to_section
(
"Test Section"
,
"Subsection With Locked Unit"
)
self
.
assertEqual
([
"Html Child in unlocked unit"
],
self
.
course_nav
.
sequence_items
)
self
.
course_nav
.
go_to_section
(
"Test Section"
,
"Unlocked Subsection"
)
self
.
assertEqual
([
"Html Child in visible unit"
],
self
.
course_nav
.
sequence_items
)
common/test/acceptance/tests/test_studio_container.py
View file @
d447c075
...
...
@@ -11,9 +11,12 @@ from ..fixtures.course import XBlockFixtureDesc
from
..pages.studio.component_editor
import
ComponentEditorView
from
..pages.studio.utils
import
add_discussion
from
..pages.lms.courseware
import
CoursewarePage
from
..pages.lms.staff_view
import
StaffPage
from
unittest
import
skip
from
acceptance.tests.base_studio_test
import
StudioCourseTest
import
datetime
from
bok_choy.promise
import
Promise
@attr
(
'shard_1'
)
...
...
@@ -46,15 +49,15 @@ class ContainerBase(StudioCourseTest):
container
=
unit
.
xblocks
[
1
]
.
go_to_container
()
return
container
def
go_to_unit_page
(
self
):
def
go_to_unit_page
(
self
,
section_name
=
'Test Section'
,
subsection_name
=
'Test Subsection'
,
unit_name
=
'Test Unit'
):
"""
Go to the test unit page.
If make_draft is true, the unit page will be put into draft mode.
"""
self
.
outline
.
visit
()
subsection
=
self
.
outline
.
section
(
'Test Section'
)
.
subsection
(
'Test Subsection'
)
return
subsection
.
toggle_expand
()
.
unit
(
'Test Unit'
)
.
go_to
()
subsection
=
self
.
outline
.
section
(
section_name
)
.
subsection
(
subsection_name
)
return
subsection
.
toggle_expand
()
.
unit
(
unit_name
)
.
go_to
()
def
verify_ordering
(
self
,
container
,
expected_orderings
):
"""
...
...
@@ -379,6 +382,8 @@ class UnitPublishingTest(ContainerBase):
PUBLISHED_STATUS
=
"Publishing Status
\n
Published"
DRAFT_STATUS
=
"Publishing Status
\n
Draft (Unpublished changes)"
LOCKED_STATUS
=
"Publishing Status
\n
Unpublished (Staff only)"
RELEASE_TITLE_RELEASED
=
"RELEASED:"
def
setup_fixtures
(
self
):
"""
...
...
@@ -393,6 +398,8 @@ class UnitPublishingTest(ContainerBase):
self
.
course_info
[
'run'
],
self
.
course_info
[
'display_name'
]
)
past_start_date
=
datetime
.
datetime
(
1974
,
6
,
22
)
self
.
past_start_date_text
=
"Jun 22, 1974 at 00:00 UTC"
course_fix
.
add_children
(
XBlockFixtureDesc
(
'chapter'
,
'Test Section'
)
.
add_children
(
...
...
@@ -401,6 +408,20 @@ class UnitPublishingTest(ContainerBase):
XBlockFixtureDesc
(
'html'
,
'Test html'
,
data
=
self
.
html_content
)
)
)
),
XBlockFixtureDesc
(
'chapter'
,
'Unlocked Section'
,
metadata
=
{
'start'
:
past_start_date
.
isoformat
()})
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Unlocked Subsection'
)
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'Unlocked Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'problem'
,
'<problem></problem>'
,
data
=
self
.
html_content
)
)
)
),
XBlockFixtureDesc
(
'chapter'
,
'Section With Locked Unit'
)
.
add_children
(
XBlockFixtureDesc
(
'sequential'
,
'Subsection With Locked Unit'
,
metadata
=
{
'start'
:
past_start_date
.
isoformat
()})
.
add_children
(
XBlockFixtureDesc
(
'vertical'
,
'Locked Unit'
,
metadata
=
{
'visible_to_staff_only'
:
True
})
.
add_children
(
XBlockFixtureDesc
(
'discussion'
,
''
,
data
=
self
.
html_content
)
)
)
)
)
.
install
()
...
...
@@ -408,61 +429,220 @@ class UnitPublishingTest(ContainerBase):
def
test_publishing
(
self
):
"""
Test the state changes when a published unit has draft changes.
Scenario: The publish title changes based on whether or not draft content exists
Given I have a published unit with no unpublished changes
When I go to the unit page in Studio
Then the title in the Publish information box is "Published"
And the Publish button is disabled
And when I add a component to the unit
Then the title in the Publish information box is "Draft (Unpublished changes)"
And the Publish button is enabled
And when I click the Publish button
Then the title in the Publish information box is "Published"
"""
unit
=
self
.
go_to_unit_page
()
self
.
assertEqual
(
self
.
PUBLISHED_STATUS
,
unit
.
publish_title
)
self
.
_verify_publish_title
(
unit
,
self
.
PUBLISHED_STATUS
)
# Start date set in course fixture to 1970.
self
.
_verify_release_date_info
(
unit
,
self
.
RELEASE_TITLE_RELEASED
,
'Jan 01, 1970 at 00:00 UTC with Section "Test Section"'
)
# Should not be able to click on Publish action -- but I don't know how to test that it is not clickable.
# TODO: continue discussion with Muhammad and Jay about this.
# Add a component to the page so it will have unpublished changes.
add_discussion
(
unit
)
self
.
assertEqual
(
self
.
DRAFT_STATUS
,
unit
.
publish_title
)
self
.
_verify_publish_title
(
unit
,
self
.
DRAFT_STATUS
)
unit
.
publish_action
.
click
()
unit
.
wait_for_ajax
()
self
.
assertEqual
(
self
.
PUBLISHED_STATUS
,
unit
.
publish_title
)
self
.
_verify_publish_title
(
unit
,
self
.
PUBLISHED_STATUS
)
def
test_discard_changes
(
self
):
"""
Test the state after discard changes.
Scenario: The publish title changes after "Discard Changes" is clicked
Given I have a published unit with no unpublished changes
When I go to the unit page in Studio
Then the Discard Changes button is disabled
And I add a component to the unit
Then the title in the Publish information box is "Draft (Unpublished changes)"
And the Discard Changes button is enabled
And when I click the Discard Changes button
Then the title in the Publish information box is "Published"
"""
unit
=
self
.
go_to_unit_page
()
add_discussion
(
unit
)
self
.
assertEqual
(
self
.
DRAFT_STATUS
,
unit
.
publish_title
)
self
.
_verify_publish_title
(
unit
,
self
.
DRAFT_STATUS
)
unit
.
discard_changes
()
self
.
assertEqual
(
self
.
PUBLISHED_STATUS
,
unit
.
publish_title
)
self
.
_verify_publish_title
(
unit
,
self
.
PUBLISHED_STATUS
)
def
test_view_live_no_changes
(
self
):
"""
Tests viewing of live with initial published content.
Scenario: "View Live" shows published content in LMS
Given I have a published unit with no unpublished changes
When I go to the unit page in Studio
Then the View Live button is enabled
And when I click on the View Live button
Then I see the published content in LMS
"""
unit
=
self
.
go_to_unit_page
()
unit
.
view_published_version
()
self
.
assertEqual
(
1
,
self
.
courseware
.
num_xblock_components
)
self
.
assertEqual
(
'html'
,
self
.
courseware
.
xblock_component_type
(
0
))
self
.
_verify_components_visible
([
'html'
])
def
test_view_live_changes
(
self
):
"""
Tests that viewing of live with draft content does not show the draft content.
Scenario: "View Live" does not show draft content in LMS
Given I have a published unit with no unpublished changes
When I go to the unit page in Studio
And when I add a component to the unit
And when I click on the View Live button
Then I see the published content in LMS
And I do not see the unpublished component
"""
unit
=
self
.
go_to_unit_page
()
add_discussion
(
unit
)
unit
.
view_published_version
()
self
.
assertEqual
(
1
,
self
.
courseware
.
num_xblock_components
)
self
.
assertEqual
(
'html'
,
self
.
courseware
.
xblock_component_type
(
0
))
self
.
_verify_components_visible
([
'html'
])
self
.
assertEqual
(
self
.
html_content
,
self
.
courseware
.
xblock_component_html_content
(
0
))
def
test_view_live_after_publish
(
self
):
"""
Tests viewing of live after creating draft and publishing it.
Scenario: "View Live" shows newly published content
Given I have a published unit with no unpublished changes
When I go to the unit page in Studio
And when I add a component to the unit
And when I click the Publish button
And when I click on the View Live button
Then I see the newly published component
"""
unit
=
self
.
go_to_unit_page
()
add_discussion
(
unit
)
unit
.
publish_action
.
click
()
unit
.
view_published_version
()
self
.
assertEqual
(
2
,
self
.
courseware
.
num_xblock_components
)
self
.
assertEqual
(
'html'
,
self
.
courseware
.
xblock_component_type
(
0
))
self
.
assertEqual
(
'discussion'
,
self
.
courseware
.
xblock_component_type
(
1
))
self
.
_verify_components_visible
([
'html'
,
'discussion'
])
def
test_initially_unlocked_visible_to_students
(
self
):
"""
Scenario: An unlocked unit with release date in the past is visible to students
Given I have a published unlocked unit with release date in the past
When I go to the unit page in Studio
Then the unit has a warning that it is visible to students
And it is marked as "RELEASED" with release date in the past visible
And when I click on the View Live Button
And when I view the course as a student
Then I see the content in the unit
"""
unit
=
self
.
go_to_unit_page
(
"Unlocked Section"
,
"Unlocked Subsection"
,
"Unlocked Unit"
)
self
.
_verify_publish_title
(
unit
,
self
.
PUBLISHED_STATUS
)
self
.
assertTrue
(
unit
.
currently_visible_to_students
)
self
.
_verify_release_date_info
(
unit
,
self
.
RELEASE_TITLE_RELEASED
,
self
.
past_start_date_text
+
' with Section "Unlocked Section"'
)
unit
.
view_published_version
()
self
.
_verify_student_view_visible
([
'problem'
])
def
test_locked_visible_to_staff_only
(
self
):
"""
Scenario: After locking a unit with release date in the past, it is only visible to staff
Given I have a published unlocked unit with release date in the past
When I go to the unit page in Studio
And when I select "Hide from students"
Then the unit does not have a warning that it is visible to students
And when I click on the View Live Button
Then I see the content in the unit when logged in as staff
And when I view the course as a student
Then I do not see any content in the unit
"""
unit
=
self
.
go_to_unit_page
(
"Unlocked Section"
,
"Unlocked Subsection"
,
"Unlocked Unit"
)
checked
=
unit
.
toggle_staff_lock
()
self
.
assertTrue
(
checked
)
self
.
assertFalse
(
unit
.
currently_visible_to_students
)
self
.
_verify_publish_title
(
unit
,
self
.
LOCKED_STATUS
)
unit
.
view_published_version
()
# Will initially be in staff view, locked component should be visible.
self
.
_verify_components_visible
([
'problem'
])
# Switch to student view and verify not visible
self
.
_verify_student_view_locked
()
def
test_initially_locked_not_visible_to_students
(
self
):
"""
Scenario: A locked unit with release date in the past is not visible to students
Given I have a published locked unit with release date in the past
When I go to the unit page in Studio
Then the unit does not have a warning that it is visible to students
And it is marked as "RELEASED" with release date in the past visible
And when I click on the View Live Button
And when I view the course as a student
Then I do not see any content in the unit
"""
unit
=
self
.
go_to_unit_page
(
"Section With Locked Unit"
,
"Subsection With Locked Unit"
,
"Locked Unit"
)
self
.
_verify_publish_title
(
unit
,
self
.
LOCKED_STATUS
)
self
.
assertFalse
(
unit
.
currently_visible_to_students
)
self
.
_verify_release_date_info
(
unit
,
self
.
RELEASE_TITLE_RELEASED
,
self
.
past_start_date_text
+
' with Subsection "Subsection With Locked Unit"'
)
unit
.
view_published_version
()
self
.
_verify_student_view_locked
()
def
test_unlocked_visible_to_all
(
self
):
"""
Scenario: After unlocking a unit with release date in the past, it is visible to both students and staff
Given I have a published unlocked unit with release date in the past
When I go to the unit page in Studio
And when I deselect "Hide from students"
Then the unit does have a warning that it is visible to students
And when I click on the View Live Button
Then I see the content in the unit when logged in as staff
And when I view the course as a student
Then I see the content in the unit
"""
unit
=
self
.
go_to_unit_page
(
"Section With Locked Unit"
,
"Subsection With Locked Unit"
,
"Locked Unit"
)
checked
=
unit
.
toggle_staff_lock
()
self
.
assertFalse
(
checked
)
self
.
_verify_publish_title
(
unit
,
self
.
PUBLISHED_STATUS
)
self
.
assertTrue
(
unit
.
currently_visible_to_students
)
unit
.
view_published_version
()
# Will initially be in staff view, components always visible.
self
.
_verify_components_visible
([
'discussion'
])
# Switch to student view and verify visible.
self
.
_verify_student_view_visible
([
'discussion'
])
def
_verify_student_view_locked
(
self
):
"""
Verifies no component is visible when viewing as a student.
"""
StaffPage
(
self
.
browser
)
.
toggle_staff_view
()
self
.
assertEqual
(
0
,
self
.
courseware
.
num_xblock_components
)
def
_verify_student_view_visible
(
self
,
expected_components
):
"""
Verifies expected components are visible when viewing as a student.
"""
StaffPage
(
self
.
browser
)
.
toggle_staff_view
()
self
.
_verify_components_visible
(
expected_components
)
def
_verify_components_visible
(
self
,
expected_components
):
"""
Verifies the expected components are visible (and there are no extras).
"""
self
.
assertEqual
(
len
(
expected_components
),
self
.
courseware
.
num_xblock_components
)
for
index
,
component
in
enumerate
(
expected_components
):
self
.
assertEqual
(
component
,
self
.
courseware
.
xblock_component_type
(
index
))
def
_verify_release_date_info
(
self
,
unit
,
expected_title
,
expected_date
):
"""
Verifies how the release date is displayed in the publishing sidebar.
"""
self
.
assertEqual
(
expected_title
,
unit
.
release_title
)
self
.
assertEqual
(
expected_date
,
unit
.
release_date
)
def
_verify_publish_title
(
self
,
unit
,
expected_title
):
"""
Waits for the publish title to change to the expected value.
"""
def
wait_for_title_change
():
return
(
unit
.
publish_title
==
expected_title
,
unit
.
publish_title
)
Promise
(
wait_for_title_change
,
"Publish title incorrect. Found '"
+
unit
.
publish_title
+
"'"
)
.
fulfill
()
# TODO: need to work with Jay/Christine to get testing of "Preview" working.
# def test_preview(self):
...
...
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