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 ...@@ -16,6 +16,7 @@ from xmodule.modulestore.django import modulestore
from xblock.core import XBlock from xblock.core import XBlock
from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.django.request import webob_to_django_response, django_to_webob_request
from xblock.exceptions import NoSuchHandlerError from xblock.exceptions import NoSuchHandlerError
from xblock.fields import Scope
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist from xblock.runtime import Mixologist
......
...@@ -264,7 +264,7 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -264,7 +264,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
# pylint: disable=too-many-format-args # pylint: disable=too-many-format-args
return HttpResponse( return HttpResponse(
content="Couldn't parse paging parameters: enable_paging: " 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('enable_paging', 'false'),
request.REQUEST.get('page_number', 0), request.REQUEST.get('page_number', 0),
request.REQUEST.get('page_size', 0) request.REQUEST.get('page_size', 0)
...@@ -273,6 +273,8 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -273,6 +273,8 @@ def xblock_view_handler(request, usage_key_string, view_name):
content_type="text/plain", 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. # Set up the context to be passed to each XBlock's render method.
context = { context = {
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks '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): ...@@ -281,6 +283,7 @@ def xblock_view_handler(request, usage_key_string, view_name):
'root_xblock': xblock if (view_name == 'container_preview') else None, 'root_xblock': xblock if (view_name == 'container_preview') else None,
'reorderable_items': reorderable_items, 'reorderable_items': reorderable_items,
'paging': paging, 'paging': paging,
'force_render': force_render,
} }
fragment = get_preview_fragment(request, xblock, context) fragment = get_preview_fragment(request, xblock, context)
......
...@@ -231,4 +231,5 @@ def manage_library_users(request, library_key_string): ...@@ -231,4 +231,5 @@ def manage_library_users(request, library_key_string):
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES), 'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
'library_key': unicode(library_key), 'library_key': unicode(library_key),
'lib_users_url': reverse_library_url('manage_library_users', library_key_string), '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 ...@@ -113,6 +113,12 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
if aside_type != 'acid_aside' 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): class StudioPermissionsService(object):
""" """
...@@ -240,6 +246,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): ...@@ -240,6 +246,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
template_context = { template_context = {
'xblock_context': context, 'xblock_context': context,
'xblock': xblock, 'xblock': xblock,
'show_preview': context.get('show_preview', True),
'content': frag.content, 'content': frag.content,
'is_root': is_root, 'is_root': is_root,
'is_reorderable': is_reorderable, 'is_reorderable': is_reorderable,
......
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info", 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"], "js/views/paged_container", "js/views/paging_header", "js/views/paging_footer", "js/views/xblock"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter) { function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingHeader, PagingFooter, XBlockView) {
var htmlResponseTpl = _.template('' + 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">' + return '<div class="xblock" data-request-token="request_token">' +
'<div class="container-paging-header"></div>' + '<div class="container-paging-header"></div>' +
htmlResponseTpl(options) + htmlResponseTpl(options) +
...@@ -14,43 +25,43 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo ...@@ -14,43 +25,43 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
'</div>' '</div>'
} }
var makePage = function(html_parameters) {
return {
resources: [],
html: getResponseHtml(html_parameters)
};
};
var PAGE_SIZE = 3; var PAGE_SIZE = 3;
var mockFirstPage = { var mockFirstPage = makePage({
resources: [],
html: getResponseHtml({
start: 0, start: 0,
displayed: PAGE_SIZE, displayed: PAGE_SIZE,
total: PAGE_SIZE + 1 total: PAGE_SIZE + 1
}) });
};
var mockSecondPage = { var mockSecondPage = makePage({
resources: [], start: PAGE_SIZE,
html: getResponseHtml({ displayed: 1,
start: PAGE_SIZE, total: PAGE_SIZE + 1
displayed: 1, });
total: PAGE_SIZE + 1
})
};
var mockEmptyPage = { var mockEmptyPage = makePage({
resources: [], start: 0,
html: getResponseHtml({ displayed: 0,
start: 0, total: 0
displayed: 0, });
total: 0
})
};
var respondWithMockPage = function(requests) { var respondWithMockPage = function(requests, mockPage) {
var requestIndex = requests.length - 1; var requestIndex = requests.length - 1;
var request = requests[requestIndex]; if (typeof mockPage == 'undefined') {
var url = new URI(request.url); var request = requests[requestIndex];
var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value var url = new URI(request.url);
var page = queryParameters.page_number; var queryParameters = url.query(true); // Returns an object with each query parameter stored as a value
var response = page === "0" ? mockFirstPage : mockSecondPage; var page = queryParameters.page_number;
AjaxHelpers.respondWithJson(requests, response, requestIndex); mockPage = page === "0" ? mockFirstPage : mockSecondPage;
}
AjaxHelpers.respondWithJson(requests, mockPage, requestIndex);
}; };
var MockPagingView = PagedContainer.extend({ var MockPagingView = PagedContainer.extend({
...@@ -65,10 +76,26 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo ...@@ -65,10 +76,26 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
beforeEach(function () { beforeEach(function () {
var feedbackTpl = readFixtures('system-feedback.underscore'); var feedbackTpl = readFixtures('system-feedback.underscore');
setFixtures($("<script>", { id: "system-feedback-tpl", type: "text/template" }).text(feedbackTpl)); 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("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 () { describe("setPage", function () {
it('can set the current page', function () { it('can set the current page', function () {
var requests = AjaxHelpers.requests(this); var requests = AjaxHelpers.requests(this);
...@@ -304,8 +331,6 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo ...@@ -304,8 +331,6 @@ define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/mo
}); });
describe("PagingFooter", function () { describe("PagingFooter", function () {
var pagingFooter;
beforeEach(function () { beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-footer.underscore'); var pagingFooterTpl = readFixtures('paging-footer.underscore');
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl)); 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 ...@@ -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 ...@@ -16,6 +16,7 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'), mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'),
mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'), mockXBlockVisibilityEditorHtml = readFixtures('mock/mock-xblock-visibility-editor.underscore'),
PageClass = fixtures.page, PageClass = fixtures.page,
pagedSpecificTests = fixtures.paged_specific_tests,
hasVisibilityEditor = fixtures.has_visibility_editor; hasVisibilityEditor = fixtures.has_visibility_editor;
beforeEach(function () { beforeEach(function () {
...@@ -305,13 +306,9 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel ...@@ -305,13 +306,9 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
} }
); );
paginated = function () {
return containerPage instanceof PagedContainerPage;
};
getDeleteOffset = function () { getDeleteOffset = function () {
// Paginated containers will make an additional AJAX request. // Paginated containers will make an additional AJAX request.
return paginated() ? 3 : 2; return pagedSpecificTests ? 3 : 2;
}; };
getGroupElement = function () { getGroupElement = function () {
...@@ -509,6 +506,48 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel ...@@ -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 () { describe('createNewComponent ', function () {
var clickNewComponent; var clickNewComponent;
...@@ -591,6 +630,7 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel ...@@ -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 ...@@ -601,7 +641,8 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
page: ContainerPage, page: ContainerPage,
initial: 'mock/mock-container-xblock.underscore', initial: 'mock/mock-container-xblock.underscore',
add_response: 'mock/mock-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 ...@@ -612,7 +653,8 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
page: PagedContainerPage, page: PagedContainerPage,
initial: 'mock/mock-container-paged-xblock.underscore', initial: 'mock/mock-container-paged-xblock.underscore',
add_response: 'mock/mock-xblock-paged.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"], "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({ var PagedContainerView = ContainerView.extend(PagingMixin).extend({
initialize: function(options){ initialize: function(options){
var self = this; var self = this;
...@@ -24,7 +24,9 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex ...@@ -24,7 +24,9 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
bind: function() {}, bind: function() {},
// size() on backbone collections shows how many objects are in the collection, or in the case // size() on backbone collections shows how many objects are in the collection, or in the case
// of paginator, on the current page. // 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 ...@@ -47,21 +49,29 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
url: decodeURIComponent(xblockUrl) + "/" + view, url: decodeURIComponent(xblockUrl) + "/" + view,
type: 'GET', type: 'GET',
cache: false, cache: false,
data: this.getRenderParameters(options.page_number), data: this.getRenderParameters(options.page_number, options.force_render),
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
success: function(fragment) { success: function(fragment) {
self.handleXBlockFragment(fragment, options); self.handleXBlockFragment(fragment, options);
self.processPaging({ requested_page: options.page_number }); self.processPaging({ requested_page: options.page_number });
self.page.updatePreviewButton(self.collection.showChildrenPreviews);
self.page.renderAddXBlockComponents(); 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 { return {
page_size: this.page_size, page_size: this.page_size,
enable_paging: true, 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 ...@@ -70,8 +80,10 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
return Math.ceil(total_count / this.page_size); return Math.ceil(total_count / this.page_size);
}, },
setPage: function(page_number) { setPage: function(page_number, additional_options) {
this.render({ page_number: page_number}); additional_options = additional_options || {};
var options = _.extend({page_number: page_number}, additional_options);
this.render(options);
}, },
processPaging: function(options){ processPaging: function(options){
...@@ -80,13 +92,15 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex ...@@ -80,13 +92,15 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
var $element = this.$el.find('.xblock-container-paging-parameters'), var $element = this.$el.find('.xblock-container-paging-parameters'),
total = $element.data('total'), total = $element.data('total'),
displayed = $element.data('displayed'), displayed = $element.data('displayed'),
start = $element.data('start'); start = $element.data('start'),
previews = $element.data('previews');
this.collection.currentPage = options.requested_page; this.collection.currentPage = options.requested_page;
this.collection.totalCount = total; this.collection.totalCount = total;
this.collection.totalPages = this.getPageCount(total); this.collection.totalPages = this.getPageCount(total);
this.collection.start = start; this.collection.start = start;
this.collection._size = displayed; this.collection._size = displayed;
this.collection.showChildrenPreviews = previews;
this.processPagingHeaderAndFooter(); this.processPagingHeaderAndFooter();
}, },
...@@ -112,23 +126,44 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex ...@@ -112,23 +126,44 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
this.pagingFooter.render(); this.pagingFooter.render();
}, },
refresh: function(block_added) { refresh: function(xblockView, block_added, is_duplicate) {
if (block_added) { if (!block_added) {
this.collection.totalCount += 1; return
this.collection._size +=1; }
if (this.collection.totalCount == 1) { if (is_duplicate) {
this.render(); // 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 return
} }
this.collection.totalPages = this.getPageCount(this.collection.totalCount); }
var new_page = this.collection.totalPages - 1; this.collection.totalCount += 1;
// If we're on a new page due to overflow, or this is the first item, set the page. this.collection._size +=1;
if (((this.collection.currentPage) != new_page) || this.collection.totalCount == 1) { if (this.collection.totalCount == 1) {
this.setPage(new_page); this.render();
} else { return
this.pagingHeader.render(); }
this.pagingFooter.render(); 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 ...@@ -157,6 +192,19 @@ define(["jquery", "underscore", "js/views/container", "js/utils/module", "gettex
sortDisplayName: function() { sortDisplayName: function() {
return gettext("Date added"); // TODO add support for sorting 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 ...@@ -140,8 +140,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
return this.xblockView.model.urlRoot; return this.xblockView.model.urlRoot;
}, },
onXBlockRefresh: function(xblockView, block_added) { onXBlockRefresh: function(xblockView, block_added, is_duplicate) {
this.xblockView.refresh(block_added); this.xblockView.refresh(xblockView, block_added, is_duplicate);
// Update publish and last modified information from the server. // Update publish and last modified information from the server.
this.model.fetch(); this.model.fetch();
}, },
...@@ -214,7 +214,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -214,7 +214,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
parent_locator: parentLocator parent_locator: parentLocator
}); });
return $.postJSON(this.getURLRoot() + '/', requestData, return $.postJSON(this.getURLRoot() + '/', requestData,
_.bind(this.onNewXBlock, this, placeholderElement, scrollOffset)) _.bind(this.onNewXBlock, this, placeholderElement, scrollOffset, false))
.fail(function() { .fail(function() {
// Remove the placeholder if the update failed // Remove the placeholder if the update failed
placeholderElement.remove(); placeholderElement.remove();
...@@ -237,7 +237,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -237,7 +237,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
parent_locator: parentElement.data('locator') parent_locator: parentElement.data('locator')
}; };
return $.postJSON(self.getURLRoot() + '/', requestData, return $.postJSON(self.getURLRoot() + '/', requestData,
_.bind(self.onNewXBlock, self, placeholderElement, scrollOffset)) _.bind(self.onNewXBlock, self, placeholderElement, scrollOffset, true))
.fail(function() { .fail(function() {
// Remove the placeholder if the update failed // Remove the placeholder if the update failed
placeholderElement.remove(); placeholderElement.remove();
...@@ -269,10 +269,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -269,10 +269,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
this.model.fetch(); this.model.fetch();
}, },
onNewXBlock: function(xblockElement, scrollOffset, data) { onNewXBlock: function(xblockElement, scrollOffset, is_duplicate, data) {
ViewUtils.setScrollOffset(xblockElement, scrollOffset); ViewUtils.setScrollOffset(xblockElement, scrollOffset);
xblockElement.data('locator', data.locator); 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 ...@@ -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 element An element representing the xblock to be refreshed.
* @param block_added Flag to indicate that new block has been just added. * @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), var xblockElement = this.findXBlockElement(element),
parentElement = xblockElement.parent(), parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id; rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) { if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({refresh: true, block_added: block_added}); this.render({refresh: true, block_added: block_added});
} else if (parentElement.hasClass('reorderable-container')) { } else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement, block_added); this.refreshChildXBlock(xblockElement, block_added, is_duplicate);
} else { } else {
this.refreshXBlock(this.findXBlockElement(parentElement)); this.refreshXBlock(this.findXBlockElement(parentElement));
} }
...@@ -303,7 +303,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -303,7 +303,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
* refreshing. * refreshing.
* @returns {jQuery promise} A promise representing the complete operation. * @returns {jQuery promise} A promise representing the complete operation.
*/ */
refreshChildXBlock: function(xblockElement, block_added) { refreshChildXBlock: function(xblockElement, block_added, is_duplicate) {
var self = this, var self = this,
xblockInfo, xblockInfo,
TemporaryXBlockView, TemporaryXBlockView,
...@@ -329,7 +329,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -329,7 +329,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}); });
return temporaryView.render({ return temporaryView.render({
success: function() { success: function() {
self.onXBlockRefresh(temporaryView, block_added); self.onXBlockRefresh(temporaryView, block_added, is_duplicate);
temporaryView.unbind(); // Remove the temporary view temporaryView.unbind(); // Remove the temporary view
} }
}); });
......
...@@ -6,11 +6,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views ...@@ -6,11 +6,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views
'use strict'; 'use strict';
var PagedXBlockContainerPage = XBlockContainerPage.extend({ var PagedXBlockContainerPage = XBlockContainerPage.extend({
events: {"click .toggle-preview-button": "toggleChildrenPreviews"},
defaultViewClass: PagedContainerView, defaultViewClass: PagedContainerView,
components_on_init: false, components_on_init: false,
initialize: function (options){ initialize: function (options){
this.events = _.extend({}, XBlockContainerPage.prototype.events, this.events);
this.page_size = options.page_size || 10; this.page_size = options.page_size || 10;
this.showChildrenPreviews = options.showChildrenPreviews || true;
XBlockContainerPage.prototype.initialize.call(this, options); XBlockContainerPage.prototype.initialize.call(this, options);
}, },
...@@ -21,16 +24,28 @@ define(["jquery", "underscore", "gettext", "js/views/pages/container", "js/views ...@@ -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), var xblockElement = this.findXBlockElement(element),
rootLocator = this.xblockView.model.id; rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) { if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({refresh: true, block_added: block_added}); this.render({refresh: true, block_added: block_added});
} else { } 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; return PagedXBlockContainerPage;
}); });
...@@ -356,6 +356,10 @@ ...@@ -356,6 +356,10 @@
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid $gray-l4; border-bottom: 1px solid $gray-l4;
background-color: $gray-l6; background-color: $gray-l6;
&.is-collapsed {
border-bottom: 0;
border-radius: 3px;
}
} }
.xblock-render { .xblock-render {
......
...@@ -39,6 +39,12 @@ ...@@ -39,6 +39,12 @@
<span class="action-button-text">Add Component</span> <span class="action-button-text">Add Component</span>
</a> </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> </ul>
</nav> </nav>
</header> </header>
......
...@@ -27,7 +27,8 @@ from django.utils.translation import ugettext as _ ...@@ -27,7 +27,8 @@ from django.utils.translation import ugettext as _
{ {
isUnitPage: false, isUnitPage: false,
page_size: 10, 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 _ ...@@ -53,7 +54,13 @@ from django.utils.translation import ugettext as _
<a href="#" class="button new-button new-component-button"> <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> <i class="icon fa fa-plus icon-inline"></i> <span class="action-button-text">${_("Add Component")}</span>
</a> </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> </ul>
</nav> </nav>
</header> </header>
......
...@@ -47,7 +47,11 @@ messages = json.dumps(xblock.validate().to_json()) ...@@ -47,7 +47,11 @@ messages = json.dumps(xblock.validate().to_json())
% endif % endif
<header class="xblock-header xblock-header-${xblock.category}"> <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"> <div class="header-details">
% if show_inline: % if show_inline:
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse"> <a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
...@@ -128,14 +132,17 @@ messages = json.dumps(xblock.validate().to_json()) ...@@ -128,14 +132,17 @@ messages = json.dumps(xblock.validate().to_json())
<div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/> <div class="wrapper-xblock-message xblock-validation-messages" data-locator="${xblock.location | h}"/>
% endif % endif
% if is_root or not xblock_url: % if show_preview:
<article class="xblock-render"> % if is_root or not xblock_url:
${content} <article class="xblock-render">
</article> ${content}
% else: </article>
<div class="xblock-message-area"> % else:
${content} <div class="xblock-message-area">
% endif ${content}
</div>
% endif
% endif
% if not is_root: % if not is_root:
</section> </section>
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
""" """
import logging 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 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__) log = logging.getLogger(__name__)
# Make '_' a no-op so we can scrape strings # Make '_' a no-op so we can scrape strings
...@@ -32,6 +33,12 @@ class LibraryRoot(XBlock): ...@@ -32,6 +33,12 @@ class LibraryRoot(XBlock):
scope=Scope.settings, scope=Scope.settings,
xml_node=True, 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_children = True
has_author_view = True has_author_view = True
...@@ -69,10 +76,22 @@ class LibraryRoot(XBlock): ...@@ -69,10 +76,22 @@ class LibraryRoot(XBlock):
children_to_show = self.children[item_start:item_end] # pylint: disable=no-member 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 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 = self.runtime.get_block(child_key)
child_view_name = StudioEditableModule.get_preview_view_name(child) 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) fragment.add_frag_resources(rendered_child)
contents.append({ contents.append({
...@@ -87,7 +106,8 @@ class LibraryRoot(XBlock): ...@@ -87,7 +106,8 @@ class LibraryRoot(XBlock):
'can_add': can_add, 'can_add': can_add,
'first_displayed': item_start, 'first_displayed': item_start,
'total_children': children_count, '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): ...@@ -106,3 +126,9 @@ class LibraryRoot(XBlock):
Always returns the raw 'library' field from the key. Always returns the raw 'library' field from the key.
""" """
return self.scope_ids.usage_id.course_key.library 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): ...@@ -500,6 +500,12 @@ class XBlockWrapper(PageObject):
""" """
self.q(css=self._bounded_selector('span.message-text a')).first.click() 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 @property
def group_configuration_link_name(self): def group_configuration_link_name(self):
""" """
......
...@@ -69,6 +69,25 @@ class LibraryEditPage(LibraryPage, PaginatedMixin): ...@@ -69,6 +69,25 @@ class LibraryEditPage(LibraryPage, PaginatedMixin):
""" """
return self._get_xblocks() 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): def click_duplicate_button(self, xblock_id):
""" """
Click on the duplicate button for the given XBlock Click on the duplicate button for the given XBlock
......
...@@ -51,7 +51,6 @@ class LibraryEditPageTest(StudioLibraryTest): ...@@ -51,7 +51,6 @@ class LibraryEditPageTest(StudioLibraryTest):
Then one XBlock is displayed Then one XBlock is displayed
And displayed XBlock are second one And displayed XBlock are second one
""" """
self.browser.save_screenshot('library_page')
self.assertEqual(len(self.lib_page.xblocks), 0) self.assertEqual(len(self.lib_page.xblocks), 0)
# Create a new block: # Create a new block:
...@@ -343,6 +342,161 @@ class LibraryNavigationTest(StudioLibraryTest): ...@@ -343,6 +342,161 @@ class LibraryNavigationTest(StudioLibraryTest):
self.assertEqual(self.lib_page.xblocks[-1].name, '11') self.assertEqual(self.lib_page.xblocks[-1].name, '11')
self.assertEqual(self.lib_page.get_page_number(), '1') 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): class LibraryUsersPageTest(StudioLibraryTest):
""" """
......
...@@ -8,7 +8,12 @@ ...@@ -8,7 +8,12 @@
</script> </script>
% endfor % 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> <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