New Publishing controls on Unit page.
STUD-1707
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 one of three values, 'make_public, 'make_private', or 'create_draft'
|
||||
:publish: can be only 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
268
cms/static/js/spec/views/pages/container_subviews_spec.js
Normal file
268
cms/static/js/spec/views/pages/container_subviews_spec.js
Normal file
@@ -0,0 +1,268 @@
|
||||
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"],
|
||||
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",
|
||||
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
|
||||
mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
|
||||
|
||||
beforeEach(function () {
|
||||
edit_helpers.installTemplate('xblock-string-field-editor');
|
||||
edit_helpers.installTemplate('publish-xblock');
|
||||
appendSetFixtures(mockContainerPage);
|
||||
|
||||
model = new XBlockInfo({
|
||||
id: 'locator-container',
|
||||
display_name: 'Test Container',
|
||||
category: 'vertical',
|
||||
published: false,
|
||||
has_changes: false
|
||||
});
|
||||
containerPage = new ContainerPage({
|
||||
model: model,
|
||||
templates: edit_helpers.mockComponentTemplates,
|
||||
el: $('#content'),
|
||||
isUnitPage: true
|
||||
});
|
||||
});
|
||||
|
||||
renderContainerPage = function(html, that) {
|
||||
requests = create_sinon.requests(that);
|
||||
containerPage.render();
|
||||
respondWithHtml(html);
|
||||
};
|
||||
|
||||
respondWithHtml = function(html) {
|
||||
var requestIndex = requests.length - 1;
|
||||
create_sinon.respondWithJson(
|
||||
requests,
|
||||
{ html: html, "resources": [] },
|
||||
requestIndex
|
||||
);
|
||||
};
|
||||
|
||||
respondWithJson = function(json) {
|
||||
var requestIndex = requests.length - 1;
|
||||
create_sinon.respondWithJson(
|
||||
requests,
|
||||
json,
|
||||
requestIndex
|
||||
);
|
||||
};
|
||||
|
||||
fetch = function (json) {
|
||||
model.fetch();
|
||||
respondWithJson(json);
|
||||
};
|
||||
|
||||
describe("PreviewActionController", function () {
|
||||
var viewPublishedCss = '.view-button',
|
||||
previewCss = '.preview-button';
|
||||
|
||||
it('renders correctly for private unit', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
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});
|
||||
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
|
||||
|
||||
fetch({"id": "locator-container", "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});
|
||||
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
|
||||
|
||||
fetch({"id": "locator-container", "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});
|
||||
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VisibilityStateController", function () {
|
||||
var unitVisibilityCss = '.section-item.editing a';
|
||||
|
||||
it('renders initially as private with unpublished content', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('private-item');
|
||||
});
|
||||
|
||||
it('renders as public when published and no changes', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": true, "has_changes": false});
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('public-item');
|
||||
});
|
||||
|
||||
it('renders as draft when published and changes', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": true, "has_changes": true});
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('draft-item');
|
||||
});
|
||||
|
||||
it('renders as private when not published', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": false, "has_changes": true});
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('private-item');
|
||||
|
||||
fetch({"id": "locator-container", "published": false, "has_changes": false});
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('private-item');
|
||||
|
||||
fetch({"id": "locator-container", "published": false});
|
||||
expect(containerPage.$(unitVisibilityCss)).toHaveClass('private-item');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Publisher", function () {
|
||||
var headerCss = '.pub-status',
|
||||
bitPublishingCss = "div.bit-publishing",
|
||||
publishedBit = "published",
|
||||
draftBit = "draft",
|
||||
publishButtonCss = ".action-publish",
|
||||
discardChangesButtonCss = ".action-discard",
|
||||
request, lastRequest, promptSpies;
|
||||
|
||||
lastRequest = function() { return requests[requests.length - 1]; };
|
||||
|
||||
beforeEach(function() {
|
||||
promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"]);
|
||||
promptSpies.show.andReturn(this.promptSpies);
|
||||
});
|
||||
|
||||
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)');
|
||||
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
|
||||
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
|
||||
};
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": false, "has_changes": false});
|
||||
verifyPrivateState();
|
||||
|
||||
fetch({"id": "locator-container", "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});
|
||||
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});
|
||||
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
|
||||
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
|
||||
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss);
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
// Click publish
|
||||
containerPage.$(publishButtonCss).click();
|
||||
edit_helpers.verifyNotificationShowing(notificationSpy, /Publishing/);
|
||||
|
||||
request = lastRequest();
|
||||
expect(request.url).toEqual("/xblock/locator-container");
|
||||
expect(request.method).toEqual("POST");
|
||||
expect(JSON.parse(request.requestBody).publish).toEqual("make_public");
|
||||
|
||||
// Response to publish call
|
||||
respondWithJson({"id": "locator-container", "published": true, "has_changes": false});
|
||||
edit_helpers.verifyNotificationHidden(notificationSpy);
|
||||
|
||||
request = lastRequest();
|
||||
expect(request.url).toEqual("/xblock/locator-container");
|
||||
expect(request.method).toEqual("GET");
|
||||
expect(request.requestBody).toEqual(null);
|
||||
// Response to fetch
|
||||
respondWithJson({"id": "locator-container", "published": true, "has_changes": false});
|
||||
|
||||
// Verify updates displayed
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
|
||||
});
|
||||
|
||||
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(draftBit);
|
||||
|
||||
// Click publish
|
||||
containerPage.$(publishButtonCss).click();
|
||||
|
||||
var numRequests = requests.length;
|
||||
// Respond with failure
|
||||
create_sinon.respondWithError(requests);
|
||||
|
||||
expect(requests.length).toEqual(numRequests);
|
||||
|
||||
// Verify still in draft state.
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
|
||||
});
|
||||
|
||||
it('can discard changes', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": true, "has_changes": true});
|
||||
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
|
||||
// Click discard changes
|
||||
containerPage.$(discardChangesButtonCss).click();
|
||||
|
||||
// Confirm the discard.
|
||||
expect(promptSpies.constructor).toHaveBeenCalled();
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(promptSpies);
|
||||
|
||||
request = lastRequest();
|
||||
expect(request.url).toEqual("/xblock/locator-container");
|
||||
expect(request.method).toEqual("DELETE");
|
||||
expect(request.requestBody).toEqual(null);
|
||||
|
||||
// Respond with failure because code does window.location.reload (which will
|
||||
// put tests into an infinite loop) on success.
|
||||
var numRequests = requests.length;
|
||||
// Respond with failure
|
||||
create_sinon.respondWithError(requests);
|
||||
|
||||
expect(requests.length).toEqual(numRequests);
|
||||
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
|
||||
});
|
||||
|
||||
it('does not discard changes on cancel', function () {
|
||||
renderContainerPage(mockContainerXBlockHtml, this);
|
||||
fetch({"id": "locator-container", "published": true, "has_changes": true});
|
||||
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
|
||||
var numRequests = requests.length;
|
||||
|
||||
// Click discard changes
|
||||
containerPage.$(discardChangesButtonCss).click();
|
||||
|
||||
// Click cancel to confirmation.
|
||||
expect(promptSpies.constructor).toHaveBeenCalled();
|
||||
promptSpies.constructor.mostRecentCall.args[0].actions.secondary.click(promptSpies);
|
||||
|
||||
expect(requests.length).toEqual(numRequests);
|
||||
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
167
cms/static/js/views/pages/container_subviews.js
Normal file
167
cms/static/js/views/pages/container_subviews.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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();
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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-bit">
|
||||
<div class="wrapper-unit-tree-location bar-mod-content">
|
||||
<h5 class="title">Unit Tree Location</h5>
|
||||
<ol>
|
||||
<li class="section">
|
||||
|
||||
@@ -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>
|
||||
|
||||
51
cms/templates/js/publish-xblock.underscore
Normal file
51
cms/templates/js/publish-xblock.underscore
Normal file
@@ -0,0 +1,51 @@
|
||||
<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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user