Commit d65e887d by Andy Armstrong Committed by cahrens

New Publishing controls on Unit page.

STUD-1707
parent b1eccdf2
......@@ -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) {
......
......@@ -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) {
......
/**
* 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&hellip;'),
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>
......
<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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment