Commit d6475f45 by cahrens

New publishing controls on unit page (continued)

STUD-1707
parent bbcc0030
from __future__ import absolute_import
"""
Helper methods for Studio views.
"""
import logging
from __future__ import absolute_import
from django.conf import settings
from django.http import HttpResponse
......@@ -13,11 +15,6 @@ from contentstore.utils import reverse_course_url, reverse_usage_url
__all__ = ['edge', 'event', 'landing']
EDITING_TEMPLATES = [
"basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor",
]
# points to the temporary course landing page with log in and sign up
def landing(request, org, course, coursename):
......
......@@ -23,6 +23,8 @@ from contentstore.tests.utils import CourseTestCase
from student.tests.factories import UserFactory
from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore import PublishState
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDIO_VIEW, STUDENT_VIEW
from xblock.exceptions import NoSuchHandlerError
from opaque_keys.edx.keys import UsageKey, CourseKey
......@@ -567,7 +569,7 @@ class TestEditItem(ItemTest):
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)
published = modulestore().get_item(published.location, revision=ModuleStoreEnum.RevisionOption.published_only)
self.assertIsNone(published.due)
def test_make_public_with_update(self):
......@@ -620,7 +622,7 @@ class TestEditItem(ItemTest):
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)
published = modulestore().get_item(published.location, revision=ModuleStoreEnum.RevisionOption.published_only)
self.assertNotEqual(draft.data, published.data)
def test_publish_states_of_nested_xblocks(self):
......
......@@ -44,8 +44,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
);
};
respondWithJson = function(json) {
var requestIndex = requests.length - 1;
respondWithJson = function(json, requestIndex) {
create_sinon.respondWithJson(
requests,
json,
......@@ -131,10 +130,29 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
draftBit = "draft",
publishButtonCss = ".action-publish",
discardChangesButtonCss = ".action-discard",
request, lastRequest, promptSpies;
request, lastRequest, promptSpies, sendDiscardChangesToServer;
lastRequest = function() { return requests[requests.length - 1]; };
sendDiscardChangesToServer = function(test) {
// Helper function to do the discard operation, up until the server response.
renderContainerPage(mockContainerXBlockHtml, test);
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);
};
beforeEach(function() {
promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"]);
promptSpies.show.andReturn(this.promptSpies);
......@@ -199,6 +217,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
// Verify updates displayed
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
});
it('can does not fetch if publish fails', function () {
......@@ -217,27 +237,26 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
// Verify still in draft state.
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
});
/* STUD-1860
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();
sendDiscardChangesToServer(this);
// Confirm the discard.
expect(promptSpies.constructor).toHaveBeenCalled();
promptSpies.constructor.mostRecentCall.args[0].actions.primary.click(promptSpies);
var numRequests = requests.length;
create_sinon.respondToDelete(requests);
// Response to fetch, specifying the very next request (as multiple requests will be sent to server)
respondWithJson({"id": "locator-container", "published": true, "has_changes": false}, numRequests);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
});
*/
request = lastRequest();
expect(request.url).toEqual("/xblock/locator-container");
expect(request.method).toEqual("DELETE");
expect(request.requestBody).toEqual(null);
it('does not fetch if discard changes fails', function () {
sendDiscardChangesToServer(this);
// 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);
......@@ -249,8 +268,6 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('does not discard changes on cancel', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": true, "has_changes": true});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
var numRequests = requests.length;
// Click discard changes
......
......@@ -28,7 +28,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/contai
if (this.isUnitPage) {
this.xblockPublisher = new ContainerSubviews.Publisher({
el: this.$('#publish-unit'),
model: this.model
model: this.model,
// When "Discard Changes" is clicked, the whole page must be re-rendered.
renderPage: this.render
});
this.xblockPublisher.render();
......
......@@ -92,6 +92,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba
BaseView.prototype.initialize.call(this);
this.template = this.loadTemplate('publish-xblock');
this.model.on('sync', this.onSync, this);
this.renderPage = this.options.renderPage;
},
onSync: function(e) {
......@@ -121,6 +122,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba
this.runOperationShowingMessage(gettext('Publishing…'),
function () {
return xblockInfo.save({publish: 'make_public'});
}).always(function() {
xblockInfo.set("publish", null);
}).done(function () {
xblockInfo.fetch();
});
......@@ -130,7 +133,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba
if (e && e.preventDefault) {
e.preventDefault();
}
var xblockInfo = this.model, view;
var xblockInfo = this.model, view, renderPage = this.renderPage;
view = new PromptView.Warning({
title: gettext("Discard Changes"),
......@@ -144,7 +147,19 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/feedba
type: 'DELETE',
url: xblockInfo.url()
}).success(function () {
return window.location.reload();
window.alert("Refresh the page to see that changes were discarded. " +
"Auto-refresh will be implemented in a later story.");
/* Fetch is never returning on sandbox-- try
doing a PUT instead of a DELETE with publish option
to work around, or contact dev ops.
STUD-1860
window.crazyAjaxHandler = xblockInfo.fetch({
complete: function(a, b, c) {
debugger;
}
});
renderPage();
*/
});
}
},
......
......@@ -11,7 +11,7 @@ else:
import json
from xmodule.modulestore import PublishState
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name, EDITING_TEMPLATES
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
from django.utils.translation import ugettext as _
%>
<%block name="title">${xblock.display_name_with_default} ${xblock_type_display_name(xblock)}</%block>
......@@ -20,16 +20,17 @@ from django.utils.translation import ugettext as _
<%namespace name='static' file='static_content.html'/>
<%namespace name="units" file="widgets/units.html" />
<%!
templates = ["basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog", "image-modal",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock"]
%>
<%block name="header_extras">
% for template_name in EDITING_TEMPLATES:
% for template_name in templates:
<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>
......@@ -40,7 +41,6 @@ from django.utils.translation import ugettext as _
"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});
// 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)}
......
......@@ -7,14 +7,14 @@
<% } %>
</h3>
<!--To be added in STUDIO-1708-->
<!--To be added in STUDIO-1826-->
<!--<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-->
<!--To be added in STUD-1829-->
<!--<div class="wrapper-release bar-mod-content">-->
<!--<h5 class="title">Will Release:</h5>-->
<!--<p class="copy">-->
......@@ -23,7 +23,7 @@
<!--</p>-->
<!--</div>-->
<!--To be added in STUD-1713-->
<!--To be added in STUD-1830-->
<!--<div class="wrapper-visibility bar-mod-content">-->
<!--<h5 class="title">Will be Visible to:</h5>-->
<!--<p class="copy">Staff and Students</p>-->
......
......@@ -11,6 +11,37 @@ class CoursewarePage(CoursePage):
"""
url_path = "courseware/"
xblock_component_selector = '.vert .xblock'
def is_browser_on_page(self):
return self.q(css='body.courseware').present
@property
def num_xblock_components(self):
"""
Return the number of rendered xblocks within the unit on the page
"""
return len(self.q(css=self.xblock_component_selector))
def xblock_component_type(self, index=0):
"""
Extract rendered xblock component type.
Returns:
str: xblock module type
index: which xblock to query, where the index is the vertical display within the page
(default is 0)
"""
return self.q(css=self.xblock_component_selector).attrs('data-block-type')[index]
def xblock_component_html_content(self, index=0):
"""
Extract rendered xblock component html content.
Returns:
str: xblock module html content
index: which xblock to query, where the index is the vertical display within the page
(default is 0)
"""
return self.q(css=self.xblock_component_selector).attrs('innerHTML')[index].strip()
......@@ -74,6 +74,64 @@ class ContainerPage(PageObject):
"""
return self._get_xblocks(".is-active ")
@property
def publish_title(self):
"""
Returns the title as displayed on the publishing sidebar component.
"""
return self.q(css='.pub-status').first.text[0]
@property
def publish_action(self):
"""
Returns the link for publishing a unit.
"""
return self.q(css='.action-publish').first
# def discard_changes(self):
# """
# Discards draft changes and reloads the page.
# NOT YET IMPLEMENTED-- part of future story
# """
#
# self.q(css='.action-discard').first.click()
#
# # TODO: work with Jay/Christine on this. I can't find something to wait on
# # that guarantees the button will be clickable.
# time.sleep(2)
#
# self.q(css='a.button.action-primary').first.click()
# self.wait_for_ajax()
#
# return ContainerPage(self.browser, self.locator).visit()
def view_published_version(self):
"""
Clicks "View Published Version", which will open the published version of the unit page in the LMS.
Switches the browser to the newly opened LMS window.
"""
self.q(css='.view-button').first.click()
self._switch_to_lms()
def preview(self):
"""
Clicks "Preview Changes", which will open the draft version of the unit page in the LMS.
Switches the browser to the newly opened LMS window.
"""
self.q(css='.preview-button').first.click()
self._switch_to_lms()
def _switch_to_lms(self):
"""
Assumes LMS has opened-- switches to that window.
"""
browser_window_handles = self.browser.window_handles
# Switch to browser window that shows HTML Unit in LMS
# The last handle represents the latest windows opened
self.browser.switch_to_window(browser_window_handles[-1])
def _get_xblocks(self, prefix=""):
return self.q(css=prefix + XBlockWrapper.BODY_SELECTOR).map(
lambda el: XBlockWrapper(self.browser, el.get_attribute('data-locator'))).results
......
......@@ -59,7 +59,7 @@ def press_the_notification_button(page, name):
page.wait_for_ajax()
def add_discussion(page, menu_index):
def add_discussion(page, menu_index=0):
"""
Add a new instance of the discussion category.
......
"""
Acceptance tests for Studio related to the container page.
The container page is used both for display units, and for
displaying containers within units.
"""
from nose.plugins.attrib import attr
......@@ -8,6 +10,7 @@ from ..fixtures.course import XBlockFixtureDesc
from ..pages.studio.component_editor import ComponentEditorView
from ..pages.studio.utils import add_discussion
from ..pages.lms.courseware import CoursewarePage
from unittest import skip
from acceptance.tests.base_studio_test import StudioCourseTest
......@@ -366,3 +369,102 @@ class EditContainerTest(NestedVerticalTest):
"""
container = self.go_to_nested_container_page()
self.modify_display_name_and_verify(container)
class UnitPublishingTest(ContainerBase):
"""
Tests of the publishing control and related widgets on the Unit page.
"""
def setup_fixtures(self):
"""
Sets up a course structure with a unit and a single HTML child.
"""
self.html_content = '<p><strong>Body of HTML Unit.</strong></p>'
self.courseware = CoursewarePage(self.browser, self.course_id)
course_fix = CourseFixture(
self.course_info['org'],
self.course_info['number'],
self.course_info['run'],
self.course_info['display_name']
)
course_fix.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('html', 'Test html', data=self.html_content)
)
)
)
).install()
self.user = course_fix.user
def test_publishing(self):
"""
Test the state changes when a published unit has draft changes.
"""
unit = self.go_to_unit_page()
self.assertEqual("Publishing Status\nPublished", unit.publish_title)
# Should not be able to click on Publish action -- but I don't know how to test that it is not clickable.
# TODO: continue discussion with Muhammad and Jay about this.
# Add a component to the page so it will have unpublished changes.
add_discussion(unit)
self.assertEqual("Publishing Status\nDraft (Unpublished changes)", unit.publish_title)
unit.publish_action.click()
unit.wait_for_ajax()
self.assertEqual("Publishing Status\nPublished", unit.publish_title)
# TODO: part of future story.
# def test_discard_changes(self):
# """
# Test the state after discard changes.
# """
# unit = self.go_to_unit_page()
# add_discussion(unit)
# unit = unit.discard_changes()
# self.assertEqual("Publishing Status\nPublished", unit.publish_title)
def test_view_live_no_changes(self):
"""
Tests viewing of live with initial published content.
"""
unit = self.go_to_unit_page()
unit.view_published_version()
self.assertEqual(1, self.courseware.num_xblock_components)
self.assertEqual('html', self.courseware.xblock_component_type(0))
def test_view_live_changes(self):
"""
Tests that viewing of live with draft content does not show the draft content.
"""
unit = self.go_to_unit_page()
add_discussion(unit)
unit.view_published_version()
self.assertEqual(1, self.courseware.num_xblock_components)
self.assertEqual('html', self.courseware.xblock_component_type(0))
self.assertEqual(self.html_content, self.courseware.xblock_component_html_content(0))
def test_view_live_after_publish(self):
"""
Tests viewing of live after creating draft and publishing it.
"""
unit = self.go_to_unit_page()
add_discussion(unit)
unit.publish_action.click()
unit.view_published_version()
self.assertEqual(2, self.courseware.num_xblock_components)
self.assertEqual('html', self.courseware.xblock_component_type(0))
self.assertEqual('discussion', self.courseware.xblock_component_type(1))
# TODO: need to work with Jay/Christine to get testing of "Preview" working.
# def test_preview(self):
# unit = self.go_to_unit_page()
# add_discussion(unit)
# unit.preview()
# self.assertEqual(2, self.courseware.num_xblock_components)
# self.assertEqual('html', self.courseware.xblock_component_type(0))
# self.assertEqual('discussion', self.courseware.xblock_component_type(1))
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