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
d65e887d
Commit
d65e887d
authored
Jul 03, 2014
by
Andy Armstrong
Committed by
cahrens
Aug 07, 2014
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
New Publishing controls on Unit page.
STUD-1707
parent
b1eccdf2
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
505 additions
and
217 deletions
+505
-217
cms/djangoapps/contentstore/features/video.py
+1
-0
cms/djangoapps/contentstore/views/component.py
+33
-6
cms/djangoapps/contentstore/views/item.py
+32
-34
cms/djangoapps/contentstore/views/tests/test_item.py
+7
-132
cms/static/coffee/spec/main.coffee
+1
-0
cms/static/js/models/xblock_info.js
+42
-2
cms/static/js/spec/views/pages/container_spec.js
+13
-2
cms/static/js/spec/views/pages/container_subviews_spec.js
+0
-0
cms/static/js/views/container.js
+3
-1
cms/static/js/views/pages/container.js
+36
-8
cms/static/js/views/pages/container_subviews.js
+167
-0
cms/static/js/views/xblock_string_field_editor.js
+2
-1
cms/templates/container.html
+34
-22
cms/templates/js/mock/mock-container-page.underscore
+56
-6
cms/templates/js/publish-xblock.underscore
+51
-0
cms/templates/studio_xblock_wrapper.html
+1
-1
common/lib/xmodule/xmodule/modulestore/mixed.py
+1
-1
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
+24
-0
lms/templates/studio_render_children_view.html
+1
-1
No files found.
cms/djangoapps/contentstore/features/video.py
View file @
d65e887d
...
...
@@ -152,6 +152,7 @@ def xml_only_video(step):
category
=
'video'
,
data
=
'<video youtube="1.00:
%
s"></video>'
%
youtube_id
,
modulestore
=
store
,
user_id
=
world
.
scenario_dict
[
"USER"
]
.
id
)
...
...
cms/djangoapps/contentstore/views/component.py
View file @
d65e887d
...
...
@@ -23,6 +23,7 @@ from xblock.runtime import Mixologist
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
from
models.settings.course_grading
import
CourseGradingModel
from
opaque_keys.edx.keys
import
UsageKey
...
...
@@ -148,7 +149,7 @@ def container_handler(request, usage_key_string):
usage_key
=
UsageKey
.
from_string
(
usage_key_string
)
try
:
course
,
xblock
,
__
=
_get_item_in_course
(
request
,
usage_key
)
course
,
xblock
,
lms_link
=
_get_item_in_course
(
request
,
usage_key
)
except
ItemNotFoundError
:
return
HttpResponseBadRequest
()
...
...
@@ -166,15 +167,38 @@ def container_handler(request, usage_key_string):
parent
=
get_parent_xblock
(
parent
)
ancestor_xblocks
.
reverse
()
subsection
=
get_parent_xblock
(
unit
)
if
unit
else
None
section
=
get_parent_xblock
(
subsection
)
if
subsection
else
None
# TODO: correct with publishing story.
unit_publish_state
=
'draft'
assert
unit
is
not
None
,
"Could not determine unit page"
subsection
=
get_parent_xblock
(
unit
)
assert
subsection
is
not
None
,
"Could not determine parent subsection from unit "
+
unicode
(
unit
.
location
)
section
=
get_parent_xblock
(
subsection
)
assert
section
is
not
None
,
"Could not determine ancestor section from unit "
+
unicode
(
unit
.
location
)
xblock_info
=
create_xblock_info
(
usage_key
,
xblock
)
# Create the link for preview.
preview_lms_base
=
settings
.
FEATURES
.
get
(
'PREVIEW_LMS_BASE'
)
# need to figure out where this item is in the list of children as the
# preview will need this
index
=
1
for
child
in
subsection
.
get_children
():
if
child
.
location
==
unit
.
location
:
break
index
+=
1
preview_lms_link
=
(
u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
)
.
format
(
preview_lms_base
=
preview_lms_base
,
lms_base
=
settings
.
LMS_BASE
,
org
=
course
.
location
.
org
,
course
=
course
.
location
.
course
,
course_name
=
course
.
location
.
name
,
section
=
section
.
location
.
name
,
subsection
=
subsection
.
location
.
name
,
index
=
index
)
return
render_to_response
(
'container.html'
,
{
'context_course'
:
course
,
# Needed only for display of menus at top of page.
'xblock'
:
xblock
,
'unit_publish_state'
:
unit_publish_state
,
'xblock_locator'
:
xblock
.
location
,
'unit'
:
unit
,
'is_unit_page'
:
is_unit_page
,
...
...
@@ -183,6 +207,9 @@ def container_handler(request, usage_key_string):
'new_unit_category'
:
'vertical'
,
'ancestor_xblocks'
:
ancestor_xblocks
,
'component_templates'
:
json
.
dumps
(
component_templates
),
'xblock_info'
:
xblock_info
,
'draft_preview_link'
:
preview_lms_link
,
'published_preview_link'
:
lms_link
,
})
else
:
return
HttpResponseBadRequest
(
"Only supports HTML requests"
)
...
...
cms/djangoapps/contentstore/views/item.py
View file @
d65e887d
...
...
@@ -23,9 +23,13 @@ import xmodule
from
xmodule.tabs
import
StaticTab
,
CourseTabList
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
,
InvalidLocationError
,
DuplicateItemError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
,
InvalidLocationError
from
xmodule.modulestore.inheritance
import
own_metadata
from
xmodule.x_module
import
PREVIEW_VIEWS
,
STUDIO_VIEW
,
STUDENT_VIEW
from
contentstore.utils
import
compute_publish_state
from
xmodule.modulestore
import
PublishState
from
django.contrib.auth.models
import
User
from
util.date_utils
import
get_default_time_display
from
util.json_request
import
expect_json
,
JsonResponse
...
...
@@ -92,7 +96,7 @@ 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 on
e of three values, 'make_public, 'make_private', or 'create_draft
'
:publish: can be on
ly one value-- 'make_public
'
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
...
...
@@ -183,7 +187,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
if
'application/json'
in
accept_header
:
store
=
modulestore
()
xblock
=
store
.
get_item
(
usage_key
)
is_read_only
=
_is_xblock_read_only
(
xblock
)
container_views
=
[
'container_preview'
,
'reorderable_container_child_preview'
]
# wrap the generated fragment in the xmodule_editor div so that the javascript
...
...
@@ -216,7 +219,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
context
=
{
'is_pages_view'
:
is_pages_view
,
# This setting disables the recursive wrapping of xblocks
'is_unit_page'
:
is_unit
(
xblock
),
'read_only'
:
is_read_only
,
'root_xblock'
:
xblock
if
(
view_name
==
'container_preview'
)
else
None
,
'reorderable_items'
:
reorderable_items
}
...
...
@@ -249,19 +251,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
return
HttpResponse
(
status
=
406
)
def
_is_xblock_read_only
(
xblock
):
"""
Returns true if the specified xblock is read-only, meaning that it cannot be edited.
"""
# We allow direct editing of xblocks in DIRECT_ONLY_CATEGORIES (for example, static pages).
# if xblock.category in DIRECT_ONLY_CATEGORIES:
# return False
# component_publish_state = compute_publish_state(xblock)
# return component_publish_state == PublishState.public
# TODO: correct with publishing story.
return
False
def
_save_item
(
user
,
usage_key
,
data
=
None
,
children
=
None
,
metadata
=
None
,
nullout
=
None
,
grader_type
=
None
,
publish
=
None
):
"""
...
...
@@ -287,19 +276,6 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
old_metadata
=
own_metadata
(
existing_item
)
old_content
=
existing_item
.
get_explicitly_set_fields_by_scope
(
Scope
.
content
)
if
publish
:
if
publish
==
'make_private'
:
try
:
store
.
unpublish
(
existing_item
.
location
,
user
.
id
),
except
ItemNotFoundError
:
pass
elif
publish
==
'create_draft'
:
try
:
store
.
convert_to_draft
(
existing_item
.
location
,
user
.
id
)
except
DuplicateItemError
:
pass
if
data
:
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
existing_item
.
data
=
data
...
...
@@ -555,8 +531,30 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
)
# Note that children aren't being returned until we have a use case.
return
{
'id'
:
unicode
(
module
.
location
),
'data'
:
data
,
'metadata'
:
own_metadata
(
module
)
return
create_xblock_info
(
usage_key
,
module
,
data
,
own_metadata
(
module
))
def
create_xblock_info
(
usage_key
,
xblock
,
data
=
None
,
metadata
=
None
):
"""
Creates the information needed for client-side XBlockInfo.
If data or metadata are not specified, their information will not be added
(regardless of whether or not the xblock actually has data or metadata).
"""
publish_state
=
compute_publish_state
(
xblock
)
if
xblock
else
None
xblock_info
=
{
"id"
:
unicode
(
xblock
.
location
),
"display_name"
:
xblock
.
display_name_with_default
,
"category"
:
xblock
.
category
,
"has_changes"
:
modulestore
()
.
has_changes
(
usage_key
),
"published"
:
publish_state
in
(
PublishState
.
public
,
PublishState
.
draft
),
"edited_on"
:
get_default_time_display
(
xblock
.
edited_on
)
if
xblock
.
edited_on
else
None
,
"edited_by"
:
User
.
objects
.
get
(
id
=
xblock
.
edited_by
)
.
username
if
xblock
.
edited_by
else
None
}
if
data
is
not
None
:
xblock_info
[
"data"
]
=
data
if
metadata
is
not
None
:
xblock_info
[
"metadata"
]
=
metadata
return
xblock_info
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
d65e887d
...
...
@@ -549,22 +549,6 @@ class TestEditItem(ItemTest):
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
def
test_make_private
(
self
):
""" Test making a public problem private (un-publishing it). """
# Make problem public.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
# Now make it private
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_private'
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
def
test_make_draft
(
self
):
""" Test creating a draft version of a public problem. """
# Make problem public.
...
...
@@ -574,13 +558,6 @@ class TestEditItem(ItemTest):
)
published
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
# Now make it draft, which means both versions will exist.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'create_draft'
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
draft
)
# Update the draft version and check that published is different.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
...
...
@@ -589,6 +566,9 @@ class TestEditItem(ItemTest):
updated_draft
=
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
,
verify_is_draft
=
True
)
self
.
assertEqual
(
updated_draft
.
due
,
datetime
(
2077
,
10
,
10
,
4
,
0
,
tzinfo
=
UTC
))
self
.
assertIsNone
(
published
.
due
)
# Fetch the published version again to make sure the due date is still unset.
published
=
modulestore
()
.
get_item
(
published
.
location
,
revision
=
REVISION_OPTION_PUBLISHED_ONLY
)
self
.
assertIsNone
(
published
.
due
)
def
test_make_public_with_update
(
self
):
""" Update a problem and make it public at the same time. """
...
...
@@ -602,112 +582,6 @@ class TestEditItem(ItemTest):
published
=
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
)
self
.
assertEqual
(
published
.
due
,
datetime
(
2077
,
10
,
10
,
4
,
0
,
tzinfo
=
UTC
))
def
test_make_private_with_update
(
self
):
""" Make a problem private and update it at the same time. """
# Make problem public.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
# Make problem private and update.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'metadata'
:
{
'due'
:
'2077-10-10T04:00Z'
},
'publish'
:
'make_private'
}
)
draft
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
self
.
assertEqual
(
draft
.
due
,
datetime
(
2077
,
10
,
10
,
4
,
0
,
tzinfo
=
UTC
))
def
test_create_draft_with_update
(
self
):
""" Create a draft and update it at the same time. """
# Make problem public.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
published
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
# Now make it draft, which means both versions will exist.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'metadata'
:
{
'due'
:
'2077-10-10T04:00Z'
},
'publish'
:
'create_draft'
}
)
draft
=
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
,
verify_is_draft
=
True
)
self
.
assertEqual
(
draft
.
due
,
datetime
(
2077
,
10
,
10
,
4
,
0
,
tzinfo
=
UTC
))
self
.
assertIsNone
(
published
.
due
)
def
test_create_draft_with_multiple_requests
(
self
):
"""
Create a draft request returns already created version if it exists.
"""
# Make problem public.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
public
)
# Now make it draft, which means both versions will exist.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'create_draft'
}
)
draft_1
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
draft
)
# Now check that when a user sends request to create a draft when there is already a draft version then
# user gets that already created draft instead of getting 'DuplicateItemError' exception.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'create_draft'
}
)
draft_2
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
draft
)
self
.
assertIsNotNone
(
draft_2
)
self
.
assertEqual
(
draft_1
,
draft_2
)
def
test_make_private_with_multiple_requests
(
self
):
"""
Make private requests gets proper response even if xmodule is already made private.
"""
# Make problem public.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_public'
}
)
self
.
assertIsNotNone
(
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
))
# Now make it private, and check that its version is private
resp
=
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_private'
}
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
draft_1
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
# Now check that when a user sends request to make it private when it already is private then
# user gets that private version instead of getting 'ItemNotFoundError' exception.
self
.
client
.
ajax_post
(
self
.
problem_update_url
,
data
=
{
'publish'
:
'make_private'
}
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
draft_2
=
self
.
verify_publish_state
(
self
.
problem_usage_key
,
PublishState
.
private
)
self
.
assertEqual
(
draft_1
,
draft_2
)
def
test_published_and_draft_contents_with_update
(
self
):
""" Create a draft and publish it then modify the draft and check that published content is not modified """
...
...
@@ -724,8 +598,7 @@ class TestEditItem(ItemTest):
data
=
{
'id'
:
unicode
(
self
.
problem_usage_key
),
'metadata'
:
{},
'data'
:
"<p>Problem content draft.</p>"
,
'publish'
:
'create_draft'
'data'
:
"<p>Problem content draft.</p>"
}
)
...
...
@@ -746,6 +619,9 @@ class TestEditItem(ItemTest):
# Both published and draft content should still be different
draft
=
self
.
get_item_from_modulestore
(
self
.
problem_usage_key
,
verify_is_draft
=
True
)
self
.
assertNotEqual
(
draft
.
data
,
published
.
data
)
# Fetch the published version again to make sure the data is correct.
published
=
modulestore
()
.
get_item
(
published
.
location
,
revision
=
REVISION_OPTION_PUBLISHED_ONLY
)
self
.
assertNotEqual
(
draft
.
data
,
published
.
data
)
def
test_publish_states_of_nested_xblocks
(
self
):
""" Test publishing of a unit page containing a nested xblock """
...
...
@@ -777,7 +653,6 @@ class TestEditItem(ItemTest):
data
=
{
'id'
:
unicode
(
unit_usage_key
),
'metadata'
:
{},
'publish'
:
'create_draft'
}
)
self
.
assertEqual
(
resp
.
status_code
,
200
)
...
...
cms/static/coffee/spec/main.coffee
View file @
d65e887d
...
...
@@ -229,6 +229,7 @@ define([
"js/spec/views/xblock_editor_spec"
,
"js/spec/views/pages/container_spec"
,
"js/spec/views/pages/container_subviews_spec"
,
"js/spec/views/pages/group_configurations_spec"
,
"js/spec/views/modals/base_modal_spec"
,
...
...
cms/static/js/models/xblock_info.js
View file @
d65e887d
...
...
@@ -7,12 +7,52 @@ define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
"id"
:
null
,
"display_name"
:
null
,
"category"
:
null
,
"is_draft"
:
null
,
"is_container"
:
null
,
"data"
:
null
,
"metadata"
:
null
,
"children"
:
null
"children"
:
null
,
/**
* True iff:
* 1) Edits have been made to the xblock and no published version exists.
* 2) Edits have been made to the xblock since the last published version.
*/
"has_changes"
:
null
,
/**
* True iff a published version of the xblock exists with a release date in the past,
* and the xblock is not locked.
*/
"released_to_students"
:
null
,
/**
* True iff a published version of the xblock exists.
*/
"published"
:
null
,
/**
* If true, only course staff can see the xblock regardless of publish status or
* release date status.
*/
"locked"
:
null
,
/**
* Date of last edit to this xblock. Will be the latest change to either the draft
* or the published version.
*/
"edited_on"
:
null
,
/**
* User who last edited the xblock.
*/
"edited_by"
:
null
,
/**
* If the xblock is published, the date on which it will be released to students.
*/
"release_date"
:
null
,
/**
* The xblock which is determining the release date. For instance, for a unit,
* this will either be the parent subsection or the grandparent section.
*/
"release_date_from"
:
null
}
// NOTE: 'publish' is not an attribute on XBlockInfo, but it used to signal the publish
// and discard changes actions. Therefore 'publish' cannot be introduced as an attribute.
});
return
XBlockInfo
;
});
cms/static/js/spec/views/pages/container_spec.js
View file @
d65e887d
...
...
@@ -143,7 +143,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
inlineEditDisplayName
(
updatedDisplayName
);
displayNameInput
.
change
();
// This is the response for the change operation.
create_sinon
.
respondWithJson
(
requests
,
{
});
// This is the response for the subsequent fetch operation.
create_sinon
.
respondWithJson
(
requests
,
{
"display_name"
:
updatedDisplayName
});
expect
(
displayNameInput
).
toHaveClass
(
'is-hidden'
);
expect
(
displayNameElement
).
not
.
toHaveClass
(
'is-hidden'
);
expect
(
displayNameElement
.
text
().
trim
()).
toBe
(
updatedDisplayName
);
...
...
@@ -153,8 +156,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it
(
'does not change the title when a display name update fails'
,
function
()
{
renderContainerPage
(
mockContainerXBlockHtml
,
this
);
inlineEditDisplayName
(
updatedDisplayName
);
var
initialRequests
=
requests
.
length
;
displayNameInput
.
change
();
create_sinon
.
respondWithError
(
requests
);
// No fetch operation should occur.
expect
(
initialRequests
+
1
).
toBe
(
requests
.
length
);
expect
(
displayNameElement
).
toHaveClass
(
'is-hidden'
);
expect
(
displayNameInput
).
not
.
toHaveClass
(
'is-hidden'
);
expect
(
displayNameInput
.
val
().
trim
()).
toBe
(
updatedDisplayName
);
...
...
@@ -305,14 +311,19 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
create_sinon
.
respondWithJson
(
requests
,
{});
// first request contains given component's id (to delete the component)
expect
(
requests
[
requests
.
length
-
2
].
url
).
toMatch
(
expect
(
requests
[
requests
.
length
-
3
].
url
).
toMatch
(
new
RegExp
(
"locator-component-"
+
GROUP_TO_TEST
+
(
componentIndex
+
1
))
);
// second request contains parent's id (to remove as child)
expect
(
lastRequest
()
.
url
).
toMatch
(
expect
(
requests
[
requests
.
length
-
2
]
.
url
).
toMatch
(
new
RegExp
(
"locator-group-"
+
GROUP_TO_TEST
)
);
// third request if a fetch of the container.
expect
(
lastRequest
().
url
).
toMatch
(
new
RegExp
(
"locator-container"
)
);
};
deleteComponentWithSuccess
=
function
(
componentIndex
)
{
...
...
cms/static/js/spec/views/pages/container_subviews_spec.js
0 → 100644
View file @
d65e887d
This diff is collapsed.
Click to expand it.
cms/static/js/views/container.js
View file @
d65e887d
...
...
@@ -82,7 +82,7 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
},
updateChildren
:
function
(
targetParent
,
successCallback
)
{
var
children
,
childLocators
;
var
children
,
childLocators
,
xblockInfo
=
this
.
model
;
// Find descendants with class "studio-xblock-wrapper" whose parent === targetParent.
// This is necessary to filter our grandchildren, great-grandchildren, etc.
...
...
@@ -110,6 +110,8 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
if
(
successCallback
)
{
successCallback
();
}
// Update publish and last modified information from the server.
xblockInfo
.
fetch
();
}
});
},
...
...
cms/static/js/views/pages/container.js
View file @
d65e887d
...
...
@@ -4,15 +4,15 @@
*/
define
([
"jquery"
,
"underscore"
,
"gettext"
,
"js/views/baseview"
,
"js/views/container"
,
"js/views/xblock"
,
"js/views/components/add_xblock"
,
"js/views/modals/edit_xblock"
,
"js/models/xblock_info"
,
"js/views/xblock_string_field_editor"
],
"js/views/xblock_string_field_editor"
,
"js/views/pages/container_subviews"
],
function
(
$
,
_
,
gettext
,
BaseView
,
ContainerView
,
XBlockView
,
AddXBlockComponent
,
EditXBlockModal
,
XBlockInfo
,
XBlockStringFieldEditor
)
{
XBlockStringFieldEditor
,
ContainerSubviews
)
{
var
XBlockContainerPage
=
BaseView
.
extend
({
// takes XBlockInfo as a model
view
:
'container_preview'
,
initialize
:
function
()
{
initialize
:
function
(
options
)
{
BaseView
.
prototype
.
initialize
.
call
(
this
);
this
.
nameEditor
=
new
XBlockStringFieldEditor
({
el
:
this
.
$
(
'.wrapper-xblock-field'
),
...
...
@@ -24,16 +24,39 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
model
:
this
.
model
,
view
:
this
.
view
});
this
.
isUnitPage
=
this
.
options
.
isUnitPage
;
if
(
this
.
isUnitPage
)
{
this
.
xblockPublisher
=
new
ContainerSubviews
.
Publisher
({
el
:
this
.
$
(
'#publish-unit'
),
model
:
this
.
model
});
this
.
xblockPublisher
.
render
();
// No need to render initially. This is only used for updating state
// when the unit changes visibility.
this
.
visibilityState
=
new
ContainerSubviews
.
VisibilityStateController
({
el
:
this
.
$
(
'.section-item.editing a'
),
model
:
this
.
model
});
this
.
previewActions
=
new
ContainerSubviews
.
PreviewActionController
({
el
:
this
.
$
(
'.nav-actions'
),
model
:
this
.
model
});
this
.
previewActions
.
render
();
}
},
render
:
function
(
options
)
{
var
self
=
this
,
xblockView
=
this
.
xblockView
,
loadingElement
=
this
.
$
(
'.ui-loading'
);
loadingElement
.
removeClass
(
'is-hidden'
);
loadingElement
=
this
.
$
(
'.ui-loading'
),
unitLocationTree
=
this
.
$
(
'.unit-location'
),
hiddenCss
=
'is-hidden'
;
loadingElement
.
removeClass
(
hiddenCss
);
// Hide both blocks until we know which one to show
xblockView
.
$el
.
addClass
(
'is-hidden'
);
xblockView
.
$el
.
addClass
(
hiddenCss
);
if
(
!
options
||
!
options
.
refresh
)
{
// Add actions to any top level buttons, e.g. "Edit" of the container itself.
...
...
@@ -45,11 +68,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
xblockView
.
render
({
success
:
function
()
{
xblockView
.
xblock
.
runtime
.
notify
(
"page-shown"
,
self
);
xblockView
.
$el
.
removeClass
(
'is-hidden'
);
xblockView
.
$el
.
removeClass
(
hiddenCss
);
self
.
renderAddXBlockComponents
();
self
.
onXBlockRefresh
(
xblockView
);
self
.
refreshDisplayName
();
loadingElement
.
addClass
(
'is-hidden'
);
loadingElement
.
addClass
(
hiddenCss
);
unitLocationTree
.
removeClass
(
hiddenCss
);
self
.
delegateEvents
();
}
});
...
...
@@ -71,6 +95,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
onXBlockRefresh
:
function
(
xblockView
)
{
this
.
addButtonActions
(
xblockView
.
$el
);
this
.
xblockView
.
refresh
();
// Update publish and last modified information from the server.
this
.
model
.
fetch
();
},
renderAddXBlockComponents
:
function
()
{
...
...
@@ -181,6 +207,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
xblockElement
.
remove
();
xblockView
.
updateChildren
(
parent
);
xblock
.
runtime
.
notify
(
'deleted-child'
,
parent
.
data
(
'locator'
));
// Update publish and last modified information from the server.
this
.
model
.
fetch
();
},
onNewXBlock
:
function
(
xblockElement
,
scrollOffset
,
data
)
{
...
...
cms/static/js/views/pages/container_subviews.js
0 → 100644
View file @
d65e887d
/**
* Subviews (usually small side panels) for XBlockContainerPage.
*/
define
([
"jquery"
,
"underscore"
,
"gettext"
,
"js/views/baseview"
,
"js/views/feedback_prompt"
],
function
(
$
,
_
,
gettext
,
BaseView
,
PromptView
)
{
var
disabledCss
=
"is-disabled"
;
/**
* A view that calls render when "has_changes" or "published" values in XBlockInfo have changed
* after a server sync operation.
*/
var
UnitStateListenerView
=
BaseView
.
extend
({
// takes XBlockInfo as a model
initialize
:
function
()
{
this
.
model
.
on
(
'sync'
,
this
.
onSync
,
this
);
},
onSync
:
function
(
e
)
{
if
(
e
.
changedAttributes
()
&&
((
'has_changes'
in
e
.
changedAttributes
())
||
(
'published'
in
e
.
changedAttributes
())))
{
this
.
render
();
}
},
render
:
function
()
{}
});
/**
* A controller for updating the visibility status of the unit on the RHS navigation tree.
*/
var
VisibilityStateController
=
UnitStateListenerView
.
extend
({
render
:
function
()
{
var
computeState
=
function
(
published
,
has_changes
)
{
if
(
!
published
)
{
return
"private"
;
}
else
if
(
has_changes
)
{
return
"draft"
;
}
else
{
return
"public"
;
}
};
var
state
=
computeState
(
this
.
model
.
get
(
'published'
),
this
.
model
.
get
(
'has_changes'
));
this
.
$el
.
removeClass
(
"private-item public-item draft-item"
);
this
.
$el
.
addClass
(
state
+
"-item"
);
}
});
/**
* A controller for updating the "View Live" and "Preview" buttons.
*/
var
PreviewActionController
=
UnitStateListenerView
.
extend
({
render
:
function
()
{
var
previewAction
=
this
.
$el
.
find
(
'.preview-button'
),
viewLiveAction
=
this
.
$el
.
find
(
'.view-button'
);
if
(
this
.
model
.
get
(
'published'
))
{
viewLiveAction
.
removeClass
(
disabledCss
);
}
else
{
viewLiveAction
.
addClass
(
disabledCss
);
}
if
(
this
.
model
.
get
(
'has_changes'
)
||
!
this
.
model
.
get
(
'published'
))
{
previewAction
.
removeClass
(
disabledCss
);
}
else
{
previewAction
.
addClass
(
disabledCss
);
}
}
});
/**
* Publisher is a view that supports the following:
* 1) Publishing of a draft version of an xblock.
* 2) Discarding of edits in a draft version.
* 3) Display of who last edited the xblock, and when.
* 4) Display of publish status (published, published with changes, changes with no published version).
*/
var
Publisher
=
BaseView
.
extend
({
events
:
{
'click .action-publish'
:
'publish'
,
'click .action-discard'
:
'discardChanges'
},
// takes XBlockInfo as a model
initialize
:
function
()
{
BaseView
.
prototype
.
initialize
.
call
(
this
);
this
.
template
=
this
.
loadTemplate
(
'publish-xblock'
);
this
.
model
.
on
(
'sync'
,
this
.
onSync
,
this
);
},
onSync
:
function
(
e
)
{
if
(
e
.
changedAttributes
()
&&
((
'has_changes'
in
e
.
changedAttributes
())
||
(
'published'
in
e
.
changedAttributes
())
||
(
'edited_on'
in
e
.
changedAttributes
())
||
(
'edited_by'
in
e
.
changedAttributes
())))
{
this
.
render
();
}
},
render
:
function
()
{
this
.
$el
.
html
(
this
.
template
({
has_changes
:
this
.
model
.
get
(
'has_changes'
),
published
:
this
.
model
.
get
(
'published'
),
edited_on
:
this
.
model
.
get
(
'edited_on'
),
edited_by
:
this
.
model
.
get
(
'edited_by'
)
}));
return
this
;
},
publish
:
function
(
e
)
{
var
xblockInfo
=
this
.
model
;
if
(
e
&&
e
.
preventDefault
)
{
e
.
preventDefault
();
}
this
.
runOperationShowingMessage
(
gettext
(
'Publishing…'
),
function
()
{
return
xblockInfo
.
save
({
publish
:
'make_public'
});
}).
done
(
function
()
{
xblockInfo
.
fetch
();
});
},
discardChanges
:
function
(
e
)
{
if
(
e
&&
e
.
preventDefault
)
{
e
.
preventDefault
();
}
var
xblockInfo
=
this
.
model
,
view
;
view
=
new
PromptView
.
Warning
({
title
:
gettext
(
"Discard Changes"
),
message
:
gettext
(
"Are you sure you want to discard changes and revert to the last published version?"
),
actions
:
{
primary
:
{
text
:
gettext
(
"Discard Changes"
),
click
:
function
(
view
)
{
view
.
hide
();
$
.
ajax
({
type
:
'DELETE'
,
url
:
xblockInfo
.
url
()
}).
success
(
function
()
{
return
window
.
location
.
reload
();
});
}
},
secondary
:
{
text
:
gettext
(
"Cancel"
),
click
:
function
(
view
)
{
view
.
hide
();
}
}
}
}).
show
();
}
});
return
{
'VisibilityStateController'
:
VisibilityStateController
,
'PreviewActionController'
:
PreviewActionController
,
'Publisher'
:
Publisher
};
});
// end define();
cms/static/js/views/xblock_string_field_editor.js
View file @
d65e887d
...
...
@@ -77,7 +77,8 @@ define(["jquery", "gettext", "js/views/baseview"],
function
()
{
return
xblockInfo
.
save
(
requestData
);
}).
done
(
function
()
{
xblockInfo
.
set
(
fieldName
,
newValue
);
// Update publish and last modified information from the server.
xblockInfo
.
fetch
();
});
},
...
...
cms/templates/container.html
View file @
d65e887d
...
...
@@ -26,30 +26,30 @@ from django.utils.translation import ugettext as _
<script
type=
"text/template"
id=
"${template_name}-tpl"
>
<%
static
:
include
path
=
"js/${template_name}.underscore"
/>
</script>
<script
type=
"text/template"
id=
"publish-xblock-tpl"
>
<%
static
:
include
path
=
"js/publish-xblock.underscore"
/>
</script>
% endfor
<link
rel=
"stylesheet"
type=
"text/css"
href=
"${static.url('js/vendor/timepicker/jquery.timepicker.css')}"
/>
</
%
block>
<
%
block
name=
"jsextra"
>
<link
rel=
"stylesheet"
type=
"text/css"
href=
"${static.url('js/vendor/timepicker/jquery.timepicker.css')}"
/>
<
%
main_xblock_info =
{
'
id
'
:
str
(
xblock_locator
),
'
display_name
'
:
xblock
.
display_name_with_default
,
'
category
'
:
xblock
.
category
,
};
%
>
<script
type=
'text/javascript'
>
require
([
"domReady!"
,
"jquery"
,
"js/models/xblock_info"
,
"js/views/pages/container"
,
"js/collections/component_template"
,
"xmodule"
,
"coffee/src/main"
,
"xblock/cms.runtime.v1"
],
function
(
doc
,
$
,
XBlockInfo
,
ContainerPage
,
ComponentTemplates
,
xmoduleLoader
)
{
var
templates
=
new
ComponentTemplates
(
$
{
component_templates
|
n
},
{
parse
:
true
});
var
mainXBlockInfo
=
new
XBlockInfo
(
$
{
json
.
dumps
(
main_xblock_info
)
|
n
});
// TODO: can go back to dumping on server side if easier.
var
mainXBlockInfo
=
new
XBlockInfo
(
$
{
json
.
dumps
(
xblock_info
)
|
n
});
var
isUnitPage
=
$
{
json
.
dumps
(
is_unit_page
)}
xmoduleLoader
.
done
(
function
()
{
var
view
=
new
ContainerPage
({
el
:
$
(
'#content'
),
model
:
mainXBlockInfo
,
templates
:
templates
templates
:
templates
,
isUnitPage
:
isUnitPage
});
view
.
render
();
});
...
...
@@ -86,13 +86,24 @@ main_xblock_info = {
<nav
class=
"nav-actions"
>
<h3
class=
"sr"
>
${_("Page Actions")}
</h3>
<ul>
% if not is_unit_page and not unit_publish_state == 'public':
<li
class=
"action-item action-edit nav-item"
>
<a
href=
"#"
class=
"button edit-button action-button"
>
<i
class=
"icon-pencil"
></i>
<span
class=
"action-button-text"
>
${_("Edit")}
</span>
</a>
</li>
% if is_unit_page:
<li
class=
"action-item action-view nav-item"
>
<a
href=
"${published_preview_link}"
class=
"button view-button action-button is-disabled"
>
<span
class=
"action-button-text"
>
${_("View Published Version")}
</span>
</a>
</li>
<li
class=
"action-item action-preview nav-item"
>
<a
href=
"${draft_preview_link}"
class=
"button preview-button action-button is-disabled"
>
<span
class=
"action-button-text"
>
${_("Preview Changes")}
</span>
</a>
</li>
% else:
<li
class=
"action-item action-edit nav-item"
>
<a
href=
"#"
class=
"button edit-button action-button"
>
<i
class=
"icon-pencil"
></i>
<span
class=
"action-button-text"
>
${_("Edit")}
</span>
</a>
</li>
% endif
</ul>
</nav>
...
...
@@ -103,7 +114,7 @@ main_xblock_info = {
<div
class=
"inner-wrapper"
>
<section
class=
"content-area"
>
<article
class=
"content-primary
window
"
>
<article
class=
"content-primary"
>
<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"
>
...
...
@@ -120,16 +131,17 @@ main_xblock_info = {
</div>
% endif
% if is_unit_page:
<div
class=
"unit-location"
>
<h4
class=
"header"
>
${_("Unit Location")}
</h4>
<div
class=
"wrapper-unit-id content-bit"
>
<div
id=
"publish-unit"
></div>
<div
class=
"unit-location is-hidden"
>
<h4
class=
"bar-mod-title"
>
${_("Unit Location")}
</h4>
<div
class=
"wrapper-unit-id bar-mod-content"
>
<h5
class=
"title"
>
Unit Location ID
</h5>
<p
class=
"unit-id"
>
<span
class=
"unit-id-value"
id=
"unit-location-id-input"
>
${unit.location.name}
</span>
<span
class=
"tip"
><span
class=
"sr"
>
Tip:
</span>
${_("Use this ID to link to this unit from other places in your course")}
</span>
</p>
</div>
<div
class=
"wrapper-unit-tree-location
content-bi
t"
>
<div
class=
"wrapper-unit-tree-location
bar-mod-conten
t"
>
<h5
class=
"title"
>
Unit Tree Location
</h5>
<ol>
<li
class=
"section"
>
...
...
cms/templates/js/mock/mock-container-page.underscore
View file @
d65e887d
...
...
@@ -14,12 +14,25 @@
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="action-item action-edit nav-item">
<a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
% if is_unit_page:
<li class="action-item action-view nav-item">
<a href="${published_preview_link}" class="button view-button action-button is-disabled">
<span class="action-button-text">${_("View Published Version")}</span>
</a>
</li>
<li class="action-item action-preview nav-item">
<a href="${draft_preview_link}" class="button preview-button action-button is-disabled">
<span class="action-button-text">${_("Preview Changes")}</span>
</a>
</li>
% else:
<li class="action-item action-edit nav-item">
<a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
% endif
</ul>
</nav>
</header>
...
...
@@ -37,7 +50,44 @@
</div>
</article>
<aside class="content-supplementary" role="complimentary">
<div id="publish-unit" class="window"></div>
</aside>
<div class="unit-location">
<h4 class="header">${_("Unit Location")}</h4>
<div class="wrapper-unit-id content-bit">
<h5 class="title">Unit Location ID</h5>
<p class="unit-id">
<span class="unit-id-value" id="unit-location-id-input">${unit.location.name}</span>
<span class="tip"><span class="sr">Tip: </span>${_("Use this ID to link to this unit from other places in your course")}</span>
</p>
</div>
<div class="wrapper-unit-tree-location content-bit">
<h5 class="title">Unit Tree Location</h5>
<ol>
<li class="section">
<a href="course-overview-url" class="section-item section-name">
<span class="section-name">Test Section</span>
</a>
<ol>
<li class="subsection">
<div class="section-item">
<span class="subsection-name"><span class="subsection-name-value">Test Subsection</span></span>
</div>
<ol class="sortable-unit-list">
<li class="courseware-unit unit is-draggable" data-locator="locator-container"
data-parent="" data-course-key="">
<div class="section-item editing">
<a href="unit-url" class="private-item">
<span class="unit-name">Test Container</span>
</a>
</div>
</ol>
</li>
</ol>
</li>
</ol>
</div>
</div>
</section>
</div>
</div>
...
...
cms/templates/js/publish-xblock.underscore
0 → 100644
View file @
d65e887d
<div class="bit-publishing <% if (published && !has_changes) { %>published<% } else { %>draft<%} %>">
<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)") %>
<% } %>
</h3>
<!--To be added in STUDIO-1708-->
<!--<div class="wrapper-last-draft bar-mod-content">-->
<!--<p class="copy meta">-->
<!--Draft saved on 6/15/2014 at 12:45pm by amako-->
<!--</p>-->
<!--</div>-->
<!--To be added in STUD-1712-->
<!--<div class="wrapper-release bar-mod-content">-->
<!--<h5 class="title">Will Release:</h5>-->
<!--<p class="copy">-->
<!--<span class="release-date">July 25, 2014</span> with-->
<!--<span class="release-with">Section "Week 1"</span>-->
<!--</p>-->
<!--</div>-->
<!--To be added in STUD-1713-->
<!--<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-pub-actions bar-mod-actions">
<ul class="action-list">
<li class="action-item">
<a class="action-publish action-primary <% if (published && !has_changes) { %>is-disabled<% } %>"
href=""><%= gettext("Publish") %>
</a>
</li>
<li class="action-item">
<a class="action-discard action-secondary <% if (!published || !has_changes) { %>is-disabled<% } %>"
href=""><%= gettext("Discard Changes") %>
</a>
</li>
</ul>
</div>
</div>
cms/templates/studio_xblock_wrapper.html
View file @
d65e887d
...
...
@@ -34,7 +34,7 @@ label = xblock.display_name or xblock.scope_ids.block_type
</div>
<div
class=
"header-actions"
>
<ul
class=
"actions-list"
>
% if not
xblock_context['read_only'] and not
is_root:
% if not is_root:
% if not show_inline:
<li
class=
"action-item action-edit"
>
<a
href=
"#"
class=
"edit-button action-button"
>
...
...
common/lib/xmodule/xmodule/modulestore/mixed.py
View file @
d65e887d
...
...
@@ -469,7 +469,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Create a copy of the source and mark its revision as draft.
Note: This method is to support the Mongo Modulestore and may be deprecated.
:param
source
: the location of the source (its revision must be None)
:param
location
: the location of the source (its revision must be None)
"""
store
=
self
.
_verify_modulestore_support
(
location
.
course_key
,
'convert_to_draft'
)
return
store
.
convert_to_draft
(
location
,
user_id
)
...
...
common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
View file @
d65e887d
...
...
@@ -530,6 +530,30 @@ class TestMixedModuleStore(unittest.TestCase):
self
.
assertIn
(
self
.
course_locations
[
self
.
XML_COURSEID1
],
course_ids
)
self
.
assertIn
(
self
.
course_locations
[
self
.
XML_COURSEID2
],
course_ids
)
@ddt.data
(
'draft'
)
def
test_has_changes_draft_mongo
(
self
,
default_ms
):
"""
Smoke test for has_changes with draft mongo modulestore.
Tests already exist for both split and draft in their own test files.
"""
self
.
initdb
(
default_ms
)
item
=
self
.
store
.
create_item
(
self
.
course_locations
[
self
.
MONGO_COURSEID
],
'problem'
,
block_id
=
'orphan'
)
self
.
assertTrue
(
self
.
store
.
has_changes
(
item
.
location
))
self
.
store
.
publish
(
item
.
location
,
self
.
user_id
)
self
.
assertFalse
(
self
.
store
.
has_changes
(
item
.
location
))
@ddt.data
(
'split'
)
def
test_has_changes_split
(
self
,
default_ms
):
"""
Smoke test for has_changes with split modulestore.
Tests already exist for both split and draft in their own test files.
"""
self
.
initdb
(
default_ms
)
self
.
assertTrue
(
self
.
store
.
has_changes
(
self
.
writable_chapter_location
))
# split modulestore's "publish" method is currently called "xblock_publish"
def
test_xml_get_courses
(
self
):
"""
Test that the xml modulestore only loaded the courses from the maps.
...
...
lms/templates/studio_render_children_view.html
View file @
d65e887d
...
...
@@ -7,6 +7,6 @@
% if can_reorder:
</ol>
% endif
% if can_add
and not xblock_context['read_only']
:
% if can_add:
<div
class=
"add-xblock-component new-component-item adding"
></div>
% endif
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