Commit b66a128c by Kelketek

Merge pull request #6939 from open-craft/content_libraries/previews

Add show/hide previews button to Content Libraries
parents 263dbdaf 568acb56
......@@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore
from xblock.core import XBlock
from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError
from xblock.fields import Scope
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
......
......@@ -264,7 +264,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
# pylint: disable=too-many-format-args
return HttpResponse(
content="Couldn't parse paging parameters: enable_paging: "
"%s, page_number: %s, page_size: %s".format(
"{0}, page_number: {1}, page_size: {2}".format(
request.REQUEST.get('enable_paging', 'false'),
request.REQUEST.get('page_number', 0),
request.REQUEST.get('page_size', 0)
......@@ -273,6 +273,8 @@ def xblock_view_handler(request, usage_key_string, view_name):
content_type="text/plain",
)
force_render = request.REQUEST.get('force_render', None)
# Set up the context to be passed to each XBlock's render method.
context = {
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks
......@@ -281,6 +283,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
'root_xblock': xblock if (view_name == 'container_preview') else None,
'reorderable_items': reorderable_items,
'paging': paging,
'force_render': force_render,
}
fragment = get_preview_fragment(request, xblock, context)
......
......@@ -231,4 +231,5 @@ def manage_library_users(request, library_key_string):
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
'library_key': unicode(library_key),
'lib_users_url': reverse_library_url('manage_library_users', library_key_string),
'show_children_previews': library.show_children_previews
})
......@@ -113,6 +113,12 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
if aside_type != 'acid_aside'
]
def render_child_placeholder(self, block, view_name, context):
"""
Renders a placeholder XBlock.
"""
return self.wrap_xblock(block, view_name, Fragment(), context)
class StudioPermissionsService(object):
"""
......@@ -240,6 +246,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
template_context = {
'xblock_context': context,
'xblock': xblock,
'show_preview': context.get('show_preview', True),
'content': frag.content,
'is_root': is_root,
'is_reorderable': is_reorderable,
......
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info",
"js/views/paged_container", "js/views/paging_header", "js/views/paging_footer"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter) {
"js/views/paged_container", "js/views/paging_header", "js/views/paging_footer", "js/views/xblock"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter, XBlockView) {
var htmlResponseTpl = _.template('' +
'<div class="xblock-container-paging-parameters" data-start="<%= start %>" data-displayed="<%= displayed %>" data-total="<%= total %>"/>'
'<div class="xblock-container-paging-parameters" ' +
'data-start="<%= start %>" ' +
'data-displayed="<%= displayed %>" ' +
'data-total="<%= total %>" ' +
'data-previews="<%= previews %>"></div>'
);
function getResponseHtml(options){
function getResponseHtml(override_options){
var default_options = {
start: 0,
displayed: PAGE_SIZE,
total: PAGE_SIZE + 1,
previews: true
};
var options = _.extend(default_options, override_options);
return '<div class="xblock" data-request-token="request_token">' +
'<div class="container-paging-header"></div>' +
htmlResponseTpl(options) +
......@@ -14,43 +25,43 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
'</div>'
}
var makePage = function(html_parameters) {
return {
resources: [],
html: getResponseHtml(html_parameters)
};
};
var PAGE_SIZE = 3;
var mockFirstPage = {
resources: [],
html: getResponseHtml({
var mockFirstPage = makePage({
start: 0,
displayed: PAGE_SIZE,
total: PAGE_SIZE + 1
})
};
});
var mockSecondPage = {
resources: [],
html: getResponseHtml({
start: PAGE_SIZE,
displayed: 1,
total: PAGE_SIZE + 1
})
};
var mockSecondPage = makePage({
start: PAGE_SIZE,
displayed: 1,
total: PAGE_SIZE + 1
});
var mockEmptyPage = {
resources: [],
html: getResponseHtml({
start: 0,
displayed: 0,
total: 0
})
};
var mockEmptyPage = makePage({
start: 0,
displayed: 0,
total: 0
});
var respondWithMockPage = function(requests) {
var respondWithMockPage = function(requests, mockPage) {
var requestIndex = requests.length - 1;
var request = requests[requestIndex];
var url = new URI(request.url);
var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
var page = queryParameters.page_number;
var response = page === "0" ? mockFirstPage : mockSecondPage;
AjaxHelpers.respondWithJson(requests, response, requestIndex);
if (typeof mockPage == 'undefined') {
var request = requests[requestIndex];
var url = new URI(request.url);
var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
var page = queryParameters.page_number;
mockPage = page === "0" ? mockFirstPage : mockSecondPage;
}
AjaxHelpers.respondWithJson(requests, mockPage, requestIndex);
};
var MockPagingView = PagedContainer.extend({
......@@ -65,10 +76,26 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
beforeEach(function () {
var feedbackTpl = readFixtures('system-feedback.underscore');
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTpl));
pagingContainer = new MockPagingView({ page_size: PAGE_SIZE });
pagingContainer = new MockPagingView({page_size: PAGE_SIZE});
});
describe("Container", function () {
describe("rendering", function(){
it('should set show_previews', function() {
var requests = AjaxHelpers.requests(this);
expect(pagingContainer.collection.showChildrenPreviews).toBe(true); //precondition check
pagingContainer.setPage(0);
respondWithMockPage(requests, makePage({previews: false}));
expect(pagingContainer.collection.showChildrenPreviews).toBe(false);
pagingContainer.setPage(0);
respondWithMockPage(requests, makePage({previews: true}));
expect(pagingContainer.collection.showChildrenPreviews).toBe(true);
});
});
describe("setPage", function () {
it('can set the current page', function () {
var requests = AjaxHelpers.requests(this);
......@@ -304,8 +331,6 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
});
describe("PagingFooter", function () {
var pagingFooter;
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-footer.underscore');
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
......@@ -485,5 +510,57 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
});
});
});
describe("Previews", function(){
describe("Toggle Previews", function(){
var testSendsAjax,
defaultUrl = "/preview/xblock/handler/trigger_previews";
testSendsAjax = function (show_previews) {
it("should send " + (!show_previews) + " when showChildrenPreviews was " + show_previews, function(){
var requests = AjaxHelpers.requests(this);
pagingContainer.collection.showChildrenPreviews = show_previews;
pagingContainer.togglePreviews();
AjaxHelpers.expectJsonRequest(requests, 'POST', defaultUrl, { showChildrenPreviews: !show_previews});
AjaxHelpers.respondWithJson(requests, { showChildrenPreviews: !show_previews });
});
};
testSendsAjax(true);
testSendsAjax(false);
it("should trigger render on success", function(){
spyOn(pagingContainer, 'render');
var requests = AjaxHelpers.requests(this);
pagingContainer.togglePreviews();
AjaxHelpers.respondWithJson(requests, { showChildrenPreviews: true });
expect(pagingContainer.render).toHaveBeenCalled();
});
it("should not trigger render on failure", function(){
spyOn(pagingContainer, 'render');
var requests = AjaxHelpers.requests(this);
pagingContainer.togglePreviews();
AjaxHelpers.respondWithError(requests);
expect(pagingContainer.render).not.toHaveBeenCalled();
});
it("should send force_render when new block causes page change", function() {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
spyOn(pagingContainer, 'render');
var mockXBlockInfo = new XBlockInfo({id: 'mock-location'});
var mockXBlockView = new XBlockView({model: mockXBlockInfo});
mockXBlockView.model.id = 'mock-location';
pagingContainer.refresh(mockXBlockView, true);
expect(pagingContainer.render).toHaveBeenCalled();
expect(pagingContainer.render.mostRecentCall.args[0].force_render).toEqual('mock-location');
});
});
});
});
});
......@@ -16,6 +16,7 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'),
mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'),
PageClass = fixtures.page,
pagedSpecificTests = fixtures.paged_specific_tests,
hasVisibilityEditor = fixtures.has_visibility_editor;
beforeEach(function () {
......@@ -305,13 +306,9 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
}
);
paginated = function () {
return containerPage instanceof PagedContainerPage;
};
getDeleteOffset = function () {
// Paginated containers will make an additional AJAX request.
return paginated() ? 3 : 2;
return pagedSpecificTests ? 3 : 2;
};
getGroupElement = function () {
......@@ -509,6 +506,48 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
});
});
describe("Previews", function () {
var getButtonIcon, getButtonText;
getButtonIcon = function (containerPage) {
return containerPage.$('.action-toggle-preview i');
};
getButtonText = function (containerPage) {
return containerPage.$('.action-toggle-preview .preview-text').text().trim();
};
if (pagedSpecificTests) {
it('has no text on the preview button to start with', function () {
containerPage = getContainerPage();
expect(getButtonIcon(containerPage)).toHaveClass('fa-refresh');
expect(getButtonIcon(containerPage).parent()).toHaveClass('is-hidden');
expect(getButtonText(containerPage)).toBe("");
});
function updatePreviewButtonTest(show_previews, expected_text) {
it('can set preview button to "' + expected_text + '"', function () {
containerPage = getContainerPage();
containerPage.updatePreviewButton(show_previews);
expect(getButtonText(containerPage)).toBe(expected_text);
});
}
updatePreviewButtonTest(true, 'Hide Previews');
updatePreviewButtonTest(false, 'Show Previews');
it('triggers underlying view togglePreviews when preview button clicked', function () {
containerPage = getContainerPage();
containerPage.render();
spyOn(containerPage.xblockView, 'togglePreviews');
containerPage.$('.toggle-preview-button').click();
expect(containerPage.xblockView.togglePreviews).toHaveBeenCalled();
});
}
});
describe('createNewComponent ', function () {
var clickNewComponent;
......@@ -591,6 +630,7 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
});
});
});
});
}
......@@ -601,7 +641,8 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
page: ContainerPage,
initial: 'mock/mock-container-xblock.underscore',
add_response: 'mock/mock-xblock.underscore',
has_visibility_editor: true
has_visibility_editor: true,
paged_specific_tests: false
}
);
......@@ -612,7 +653,8 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
page: PagedContainerPage,
initial: 'mock/mock-container-paged-xblock.underscore',
add_response: 'mock/mock-xblock-paged.underscore',
has_visibility_editor: false
has_visibility_editor: false,
paged_specific_tests: true
}
);
});
define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettext",
define(["jquery", "underscore", "js/views/utils/view_utils", "js/views/container", "js/utils/module", "gettext",
"js/views/feedback_notification", "js/views/paging_header", "js/views/paging_footer", "js/views/paging_mixin"],
function ($, _, ContainerView, ModuleUtils, gettext, NotificationView, PagingHeader, PagingFooter, PagingMixin) {
function ($, _, ViewUtils, ContainerView, ModuleUtils, gettext, NotificationView, PagingHeader, PagingFooter, PagingMixin) {
var PagedContainerView = ContainerView.extend(PagingMixin).extend({
initialize: function(options){
var self = this;
......@@ -24,7 +24,9 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
bind: function() {},
// size() on backbone collections shows how many objects are in the collection, or in the case
// of paginator, on the current page.
size: function() { return self.collection._size; }
size: function() { return self.collection._size; },
// Toggles the functionality for showing and hiding child previews.
showChildrenPreviews: true
};
},
......@@ -47,21 +49,29 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
url: decodeURIComponent(xblockUrl) + "/" + view,
type: 'GET',
cache: false,
data: this.getRenderParameters(options.page_number),
data: this.getRenderParameters(options.page_number, options.force_render),
headers: { Accept: 'application/json' },
success: function(fragment) {
self.handleXBlockFragment(fragment, options);
self.processPaging({ requested_page: options.page_number });
self.page.updatePreviewButton(self.collection.showChildrenPreviews);
self.page.renderAddXBlockComponents();
if (options.force_render) {
var target = $('.studio-xblock-wrapper[data-locator="' + options.force_render + '"]');
// Scroll us to the element with a little buffer at the top for context.
ViewUtils.setScrollOffset(target, ($(window).height() * .10));
}
}
});
},
getRenderParameters: function(page_number) {
getRenderParameters: function(page_number, force_render) {
// Options should at least contain page_number.
return {
page_size: this.page_size,
enable_paging: true,
page_number: page_number
page_number: page_number,
force_render: force_render
};
},
......@@ -70,8 +80,10 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
return Math.ceil(total_count / this.page_size);
},
setPage: function(page_number) {
this.render({ page_number: page_number});
setPage: function(page_number, additional_options) {
additional_options = additional_options || {};
var options = _.extend({page_number: page_number}, additional_options);
this.render(options);
},
processPaging: function(options){
......@@ -80,13 +92,15 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
var $element = this.$el.find('.xblock-container-paging-parameters'),
total = $element.data('total'),
displayed = $element.data('displayed'),
start = $element.data('start');
start = $element.data('start'),
previews = $element.data('previews');
this.collection.currentPage = options.requested_page;
this.collection.totalCount = total;
this.collection.totalPages = this.getPageCount(total);
this.collection.start = start;
this.collection._size = displayed;
this.collection.showChildrenPreviews = previews;
this.processPagingHeaderAndFooter();
},
......@@ -112,23 +126,44 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
this.pagingFooter.render();
},
refresh: function(block_added) {
if (block_added) {
this.collection.totalCount += 1;
this.collection._size +=1;
if (this.collection.totalCount == 1) {
this.render();
refresh: function(xblockView, block_added, is_duplicate) {
if (!block_added) {
return
}
if (is_duplicate) {
// Duplicated blocks can be inserted onto the current page.
var xblock = xblockView.xblock.element.parents(".studio-xblock-wrapper").first();
var all_xblocks = xblock.parent().children(".studio-xblock-wrapper");
var index = all_xblocks.index(xblock);
if ((index + 1 <= this.page_size) && (all_xblocks.length > this.page_size)) {
// Pop the last XBlock off the bottom.
all_xblocks[all_xblocks.length - 1].remove();
return
}
this.collection.totalPages = this.getPageCount(this.collection.totalCount);
var new_page = this.collection.totalPages - 1;
// If we're on a new page due to overflow, or this is the first item, set the page.
if (((this.collection.currentPage) != new_page) || this.collection.totalCount == 1) {
this.setPage(new_page);
} else {
this.pagingHeader.render();
this.pagingFooter.render();
}
this.collection.totalCount += 1;
this.collection._size +=1;
if (this.collection.totalCount == 1) {
this.render();
return
}
this.collection.totalPages = this.getPageCount(this.collection.totalCount);
var target_page = this.collection.totalPages - 1;
// If we're on a new page due to overflow, or this is the first item, set the page.
if (((this.collection.currentPage) != target_page) || this.collection.totalCount == 1) {
var force_render = xblockView.model.id;
if (is_duplicate) {
// The duplicate should be on the next page if we've gotten here.
target_page = this.collection.currentPage + 1;
}
this.setPage(
target_page,
{force_render: force_render}
);
} else {
this.pagingHeader.render();
this.pagingFooter.render();
}
},
......@@ -157,6 +192,19 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
sortDisplayName: function() {
return gettext("Date added"); // TODO add support for sorting
},
togglePreviews: function(){
var self = this,
xblockUrl = this.model.url();
return $.ajax({
// No runtime, so can't get this via the handler() call.
url: '/preview' + decodeURIComponent(xblockUrl) + "/handler/trigger_previews",
type: 'POST',
data: JSON.stringify({ showChildrenPreviews: !this.collection.showChildrenPreviews}),
dataType: 'json'
})
.then(self.render).promise();
}
});
......
......@@ -140,8 +140,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
return this.xblockView.model.urlRoot;
},
onXBlockRefresh: function(xblockView, block_added) {
this.xblockView.refresh(block_added);
onXBlockRefresh: function(xblockView, block_added, is_duplicate) {
this.xblockView.refresh(xblockView, block_added, is_duplicate);
// Update publish and last modified information from the server.
this.model.fetch();
},
......@@ -214,7 +214,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
parent_locator: parentLocator
});
return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset))
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
......@@ -237,7 +237,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
parent_locator: parentElement.data('locator')
};
return $.postJSON(self.getURLRoot() + '/', requestData,
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset))
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset, true))
.fail(function() {
// Remove the placeholder if the update failed
placeholderElement.remove();
......@@ -269,10 +269,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
this.model.fetch();
},
onNewXBlock: function(xblockElement, scrollOffset, data) {
onNewXBlock: function(xblockElement, scrollOffset, is_duplicate, data) {
ViewUtils.setScrollOffset(xblockElement, scrollOffset);
xblockElement.data('locator', data.locator);
return this.refreshXBlock(xblockElement, true);
return this.refreshXBlock(xblockElement, true, is_duplicate);
},
/**
......@@ -282,14 +282,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
* @param element An element representing the xblock to be refreshed.
* @param block_added Flag to indicate that new block has been just added.
*/
refreshXBlock: function(element, block_added) {
refreshXBlock: function(element, block_added, is_duplicate) {
var xblockElement = this.findXBlockElement(element),
parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({refresh: true, block_added: block_added});
} else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement, block_added);
this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
} else {
this.refreshXBlock(this.findXBlockElement(parentElement));
}
......@@ -303,7 +303,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
* refreshing.
* @returns {jQuery promise} A promise representing the complete operation.
*/
refreshChildXBlock: function(xblockElement, block_added) {
refreshChildXBlock: function(xblockElement, block_added, is_duplicate) {
var self = this,
xblockInfo,
TemporaryXBlockView,
......@@ -329,7 +329,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
return temporaryView.render({
success: function() {
self.onXBlockRefresh(temporaryView, block_added);
self.onXBlockRefresh(temporaryView, block_added, is_duplicate);
temporaryView.unbind(); // Remove the temporary view
}
});
......
......@@ -6,11 +6,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views
'use strict';
var PagedXBlockContainerPage = XBlockContainerPage.extend({
events: {"click .toggle-preview-button": "toggleChildrenPreviews"},
defaultViewClass: PagedContainerView,
components_on_init: false,
initialize: function (options){
this.events = _.extend({}, XBlockContainerPage.prototype.events, this.events);
this.page_size = options.page_size || 10;
this.showChildrenPreviews = options.showChildrenPreviews || true;
XBlockContainerPage.prototype.initialize.call(this, options);
},
......@@ -21,16 +24,28 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views
});
},
refreshXBlock: function(element, block_added) {
refreshXBlock: function(element, block_added, is_duplicate) {
var xblockElement = this.findXBlockElement(element),
rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({refresh: true, block_added: block_added});
} else {
this.refreshChildXBlock(xblockElement, block_added);
this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
}
}
},
toggleChildrenPreviews: function(xblockElement) {
xblockElement.preventDefault();
this.xblockView.togglePreviews();
},
updatePreviewButton: function(show_previews){
var text = (show_previews) ? gettext('Hide Previews') : gettext('Show Previews'),
button = $('.nav-actions .button-toggle-preview');
this.$(".preview-text", button).text(text);
this.$('.toggle-preview-button').removeClass("is-hidden");
}
});
return PagedXBlockContainerPage;
});
......@@ -356,6 +356,10 @@
margin-bottom: 0;
border-bottom: 1px solid $gray-l4;
background-color: $gray-l6;
&.is-collapsed {
border-bottom: 0;
border-radius: 3px;
}
}
.xblock-render {
......
......@@ -39,6 +39,12 @@
<span class="action-button-text">Add Component</span>
</a>
</li>
<li class="action-item action-toggle-preview nav-item">
<a href="#" class="button button-toggle-preview action-button toggle-preview-button is-hidden">
<i class="icon fa fa-refresh"></i>
<span class="action-button-text preview-text"></span>
</a>
</li>
</ul>
</nav>
</header>
......
......@@ -27,7 +27,8 @@ from django.utils.translation import ugettext as _
{
isUnitPage: false,
page_size: 10,
canEdit: ${"true" if can_edit else "false"}
canEdit: ${"true" if can_edit else "false"},
showChildrenPreviews: ${'true' if show_children_previews else 'false'}
}
);
});
......@@ -53,7 +54,13 @@ from django.utils.translation import ugettext as _
<a href="#" class="button new-button new-component-button">
<i class="icon fa fa-plus icon-inline"></i> <span class="action-button-text">${_("Add Component")}</span>
</a>
</li>
</li>
<li class="action-item action-toggle-preview nav-item">
<a href="#" class="button button-toggle-preview action-button toggle-preview-button is-hidden">
<i class="icon fa fa-refresh"></i>
<span class="action-button-text preview-text"></span>
</a>
</li>
</ul>
</nav>
</header>
......
......@@ -47,7 +47,11 @@ messages = json.dumps(xblock.validate().to_json())
% endif
<header class="xblock-header xblock-header-${xblock.category}">
<div class="xblock-header-primary">
<div class="xblock-header-primary
% if not show_preview:
is-collapsed
% endif
">
<div class="header-details">
% if show_inline:
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
......@@ -128,14 +132,17 @@ messages = json.dumps(xblock.validate().to_json())
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/>
% endif
% if is_root or not xblock_url:
<article class="xblock-render">
${content}
</article>
% else:
<div class="xblock-message-area">
${content}
% endif
% if show_preview:
% if is_root or not xblock_url:
<article class="xblock-render">
${content}
</article>
% else:
<div class="xblock-message-area">
${content}
</div>
% endif
% endif
% if not is_root:
</section>
......
......@@ -3,11 +3,12 @@
"""
import logging
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment
from xmodule.studio_editable import StudioEditableModule
from xblock.fields import Scope, String, List, Boolean
from xblock.fragment import Fragment
from xblock.core import XBlock
log = logging.getLogger(__name__)
# Make '_' a no-op so we can scrape strings
......@@ -32,6 +33,12 @@ class LibraryRoot(XBlock):
scope=Scope.settings,
xml_node=True,
)
show_children_previews = Boolean(
display_name="Hide children preview",
help="Choose if preview of library contents is shown",
scope=Scope.user_state,
default=True
)
has_children = True
has_author_view = True
......@@ -69,10 +76,22 @@ class LibraryRoot(XBlock):
children_to_show = self.children[item_start:item_end] # pylint: disable=no-member
force_render = context.get('force_render', None)
for child_key in children_to_show: # pylint: disable=E1101
# Children must have a separate context from the library itself. Make a copy.
child_context = context.copy()
child_context['show_preview'] = self.show_children_previews
child = self.runtime.get_block(child_key)
child_view_name = StudioEditableModule.get_preview_view_name(child)
rendered_child = self.runtime.render_child(child, child_view_name, context)
if unicode(child.location) == force_render:
child_context['show_preview'] = True
if child_context['show_preview']:
rendered_child = self.runtime.render_child(child, child_view_name, child_context)
else:
rendered_child = self.runtime.render_child_placeholder(child, child_view_name, child_context)
fragment.add_frag_resources(rendered_child)
contents.append({
......@@ -87,7 +106,8 @@ class LibraryRoot(XBlock):
'can_add': can_add,
'first_displayed': item_start,
'total_children': children_count,
'displayed_children': len(children_to_show)
'displayed_children': len(children_to_show),
'previews': self.show_children_previews
})
)
......@@ -106,3 +126,9 @@ class LibraryRoot(XBlock):
Always returns the raw 'library' field from the key.
"""
return self.scope_ids.usage_id.course_key.library
@XBlock.json_handler
def trigger_previews(self, request_body, suffix): # pylint: disable=unused-argument
""" Enable or disable previews in studio for library children. """
self.show_children_previews = request_body.get('showChildrenPreviews', self.show_children_previews)
return {'showChildrenPreviews': self.show_children_previews}
......@@ -500,6 +500,12 @@ class XBlockWrapper(PageObject):
"""
self.q(css=self._bounded_selector('span.message-text a')).first.click()
def is_placeholder(self):
"""
Checks to see if the XBlock is rendered as a placeholder without a preview.
"""
return not self.q(css=self._bounded_selector('.wrapper-xblock article')).present
@property
def group_configuration_link_name(self):
"""
......
......@@ -69,6 +69,25 @@ class LibraryEditPage(LibraryPage, PaginatedMixin):
"""
return self._get_xblocks()
def are_previews_showing(self):
"""
Determines whether or not previews are showing for XBlocks
"""
return all([not xblock.is_placeholder() for xblock in self.xblocks])
def toggle_previews(self):
"""
Clicks the preview toggling button and waits for the previews to appear or disappear.
"""
toggle = not self.are_previews_showing()
self.q(css='.toggle-preview-button').click()
EmptyPromise(
lambda: self.are_previews_showing() == toggle,
'Preview is visible: %s' % toggle,
timeout=30
).fulfill()
self.wait_until_ready()
def click_duplicate_button(self, xblock_id):
"""
Click on the duplicate button for the given XBlock
......
......@@ -51,7 +51,6 @@ class LibraryEditPageTest(StudioLibraryTest):
Then one XBlock is displayed
And displayed XBlock are second one
"""
self.browser.save_screenshot('library_page')
self.assertEqual(len(self.lib_page.xblocks), 0)
# Create a new block:
......@@ -343,6 +342,161 @@ class LibraryNavigationTest(StudioLibraryTest):
self.assertEqual(self.lib_page.xblocks[-1].name, '11')
self.assertEqual(self.lib_page.get_page_number(), '1')
def test_previews(self):
"""
Scenario: Ensure the user is able to hide previews of XBlocks.
Given that I have a library in Studio with 40 XBlocks
Then previews are visible
And when I click the toggle previews button
Then the previews will not be visible
And when I click the toggle previews button
Then the previews are visible
"""
self.assertTrue(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
self.assertFalse(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
self.assertTrue(self.lib_page.are_previews_showing())
def test_previews_navigation(self):
"""
Scenario: Ensure preview settings persist across navigation.
Given that I have a library in Studio with 40 XBlocks
Then previews are visible
And when I click the toggle previews button
And click the next page button
Then the previews will not be visible
And the first XBlock will be the 11th one
And the last XBlock will be the 20th one
And when I click the toggle previews button
And I click the previous page button
Then the previews will be visible
And the first XBlock will be the first one
And the last XBlock will be the 11th one
"""
self.assertTrue(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
# Which set of arrows shouldn't matter for this test.
self.lib_page.move_forward('top')
self.assertFalse(self.lib_page.are_previews_showing())
self.assertEqual(self.lib_page.xblocks[0].name, '11')
self.assertEqual(self.lib_page.xblocks[-1].name, '20')
self.lib_page.toggle_previews()
self.lib_page.move_back('top')
self.assertTrue(self.lib_page.are_previews_showing())
self.assertEqual(self.lib_page.xblocks[0].name, '1')
self.assertEqual(self.lib_page.xblocks[-1].name, '10')
def test_preview_state_persistance(self):
"""
Scenario: Ensure preview state persists between page loads.
Given that I have a library in Studio with 40 XBlocks
Then previews are visible
And when I click the toggle previews button
And I revisit the page
Then the previews will not be visible
"""
self.assertTrue(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
self.lib_page.visit()
self.lib_page.wait_until_ready()
self.assertFalse(self.lib_page.are_previews_showing())
def test_preview_add_xblock(self):
"""
Scenario: Ensure previews are shown when adding new blocks, regardless of preview setting.
Given that I have a library in Studio with 40 XBlocks
Then previews are visible
And when I click the toggle previews button
Then the previews will not be visible
And when I add an XBlock
Then I will be on the 5th page
And the XBlock will have loaded a preview
And when I revisit the library
And I go to the 5th page
Then the top XBlock will be the one I added
And it will not have a preview
And when I add an XBlock
Then the XBlock I added will have a preview
And the top XBlock will not have one.
"""
self.assertTrue(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
self.assertFalse(self.lib_page.are_previews_showing())
add_component(self.lib_page, "problem", "Checkboxes")
self.assertEqual(self.lib_page.get_page_number(), '5')
first_added = self.lib_page.xblocks[0]
self.assertIn("Checkboxes", first_added.name)
self.assertFalse(self.lib_page.xblocks[0].is_placeholder())
self.lib_page.visit()
self.lib_page.wait_until_ready()
self.lib_page.go_to_page(5)
self.assertTrue(self.lib_page.xblocks[0].is_placeholder())
add_component(self.lib_page, "problem", "Multiple Choice")
# DOM has detatched the element since last assignment
first_added = self.lib_page.xblocks[0]
second_added = self.lib_page.xblocks[1]
self.assertIn("Multiple Choice", second_added.name)
self.assertFalse(second_added.is_placeholder())
self.assertTrue(first_added.is_placeholder())
def test_edit_with_preview(self):
"""
Scenario: Editing an XBlock should show me a preview even if previews are hidden.
Given that I have a library in Studio with 40 XBlocks
Then previews are visible
And when I click the toggle previews button
Then the previews will not be visible
And when I edit the first XBlock
Then the first XBlock will show a preview
And the other XBlocks will still be placeholders
"""
self.assertTrue(self.lib_page.are_previews_showing())
self.lib_page.toggle_previews()
self.assertFalse(self.lib_page.are_previews_showing())
target = self.lib_page.xblocks[0]
target.edit()
target.save_settings()
self.assertFalse(target.is_placeholder())
self.assertTrue(all([xblock.is_placeholder() for xblock in self.lib_page.xblocks[1:]]))
def test_duplicate_xblock_pagination(self):
"""
Scenario: Duplicating an XBlock should not shift the page if the XBlock is not at the end.
Given that I have a library in Studio with 40 XBlocks
When I duplicate the third XBlock
Then the page should not change
And the duplicate XBlock should be there
And it should show a preview
And there should not be more than 10 XBlocks visible.
"""
third_block_id = self.lib_page.xblocks[2].locator
self.lib_page.click_duplicate_button(third_block_id)
self.lib_page.wait_until_ready()
target = self.lib_page.xblocks[3]
self.assertIn('Duplicate', target.name)
self.assertFalse(target.is_placeholder())
self.assertEqual(len(self.lib_page.xblocks), 10)
def test_duplicate_xblock_pagination_end(self):
"""
Scenario: Duplicating an XBlock if it's the last one should bring me to the next page with a preview.
Given that I have a library in Studio with 40 XBlocks
And when I hide previews
And I duplicate the last XBlock
The page should change to page 2
And the duplicate XBlock should be the first XBlock
And it should not be a placeholder
"""
self.lib_page.toggle_previews()
last_block_id = self.lib_page.xblocks[-1].locator
self.lib_page.click_duplicate_button(last_block_id)
self.lib_page.wait_until_ready()
self.assertEqual(self.lib_page.get_page_number(), '2')
target_block = self.lib_page.xblocks[0]
self.assertIn('Duplicate', target_block.name)
self.assertFalse(target_block.is_placeholder())
class LibraryUsersPageTest(StudioLibraryTest):
"""
......
......@@ -8,7 +8,12 @@
</script>
% endfor
<div class="xblock-container-paging-parameters" data-start="${first_displayed}" data-displayed="${displayed_children}" data-total="${total_children}"></div>
<div class="xblock-container-paging-parameters"
data-start="${first_displayed}"
data-displayed="${displayed_children}"
data-total="${total_children}"
data-previews="${'true' if previews else 'false'}"
></div>
<div class="container-paging-header"></div>
......
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