Commit ff1a08cb by E. Kolpakov

Paging for LibraryView added with JS tests.

parent 05817614
...@@ -237,12 +237,28 @@ def xblock_view_handler(request, usage_key_string, view_name): ...@@ -237,12 +237,28 @@ def xblock_view_handler(request, usage_key_string, view_name):
if view_name == 'reorderable_container_child_preview': if view_name == 'reorderable_container_child_preview':
reorderable_items.add(xblock.location) reorderable_items.add(xblock.location)
paging = None
try:
if request.REQUEST.get('enable_paging', 'false') == 'true':
paging = {
'page_number': int(request.REQUEST.get('page_number', 0)),
'page_size': int(request.REQUEST.get('page_size', 0)),
}
except ValueError:
log.exception(
"Couldn't parse paging parameters: enable_paging: %s, page_number: %s, page_size: %s",
request.REQUEST.get('enable_paging', 'false'),
request.REQUEST.get('page_number', 0),
request.REQUEST.get('page_size', 0)
)
# 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
'is_unit_page': is_unit(xblock), 'is_unit_page': is_unit(xblock),
'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
} }
fragment = get_preview_fragment(request, xblock, context) fragment = get_preview_fragment(request, xblock, context)
......
...@@ -239,6 +239,7 @@ define([ ...@@ -239,6 +239,7 @@ define([
"js/spec/views/assets_spec", "js/spec/views/assets_spec",
"js/spec/views/baseview_spec", "js/spec/views/baseview_spec",
"js/spec/views/container_spec", "js/spec/views/container_spec",
"js/spec/views/library_container_spec",
"js/spec/views/group_configuration_spec", "js/spec/views/group_configuration_spec",
"js/spec/views/paging_spec", "js/spec/views/paging_spec",
"js/spec/views/unit_outline_spec", "js/spec/views/unit_outline_spec",
......
define([ define([
'jquery', 'js/models/xblock_info', 'js/views/pages/container', 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main', 'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1' 'xblock/cms.runtime.v1'
], ],
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) { function($, _, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict'; 'use strict';
return function (componentTemplates, XBlockInfoJson, action, isUnitPage) { return function (componentTemplates, XBlockInfoJson, action, options) {
var templates = new ComponentTemplates(componentTemplates, {parse: true}), var main_options = {
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true});
xmoduleLoader.done(function () {
var view = new ContainerPage({
el: $('#content'), el: $('#content'),
model: mainXBlockInfo, model: new XBlockInfo(XBlockInfoJson, {parse: true}),
action: action, action: action,
templates: templates, templates: new ComponentTemplates(componentTemplates, {parse: true})
isUnitPage: isUnitPage };
});
xmoduleLoader.done(function () {
var view = new ContainerPage(_.extend(main_options, options));
view.render(); view.render();
}); });
}; };
......
define([ define([
'jquery', 'js/models/xblock_info', 'js/views/pages/container', 'jquery', 'underscore', 'js/models/xblock_info', 'js/views/pages/container',
'js/collections/component_template', 'xmodule', 'coffee/src/main', 'js/collections/component_template', 'xmodule', 'coffee/src/main',
'xblock/cms.runtime.v1' 'xblock/cms.runtime.v1'
], ],
function($, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) { function($, _, XBlockInfo, ContainerPage, ComponentTemplates, xmoduleLoader) {
'use strict'; 'use strict';
return function (componentTemplates, XBlockInfoJson) { return function (componentTemplates, XBlockInfoJson, options) {
var templates = new ComponentTemplates(componentTemplates, {parse: true}), var main_options = {
mainXBlockInfo = new XBlockInfo(XBlockInfoJson, {parse: true}); el: $('#content'),
model: new XBlockInfo(XBlockInfoJson, {parse: true}),
templates: new ComponentTemplates(componentTemplates, {parse: true}),
action: 'view'
};
xmoduleLoader.done(function () { xmoduleLoader.done(function () {
var view = new ContainerPage({ var view = new ContainerPage(_.extend(main_options, options));
el: $('#content'),
model: mainXBlockInfo,
action: "view",
templates: templates,
isUnitPage: false
});
view.render(); view.render();
}); });
}; };
......
define([ "jquery", "underscore", "js/common_helpers/ajax_helpers", "URI", "js/models/xblock_info",
"js/views/library_container", "js/views/paging_header", "js/views/paging_footer"],
function ($, _, AjaxHelpers, URI, XBlockInfo, PagedContainer, PagingContainer, PagingFooter) {
var htmlResponseTpl = _.template('' +
'<div class="xblock-container-paging-parameters" data-start="<%= start %>" data-displayed="<%= displayed %>" data-total="<%= total %>"/>'
);
function getResponseHtml(options){
return '<div class="xblock" data-request-token="request_token">' +
'<div class="container-paging-header"></div>' +
htmlResponseTpl(options) +
'<div class="container-paging-footer"></div>' +
'</div>'
}
var PAGE_SIZE = 3;
var mockFirstPage = {
resources: [],
html: getResponseHtml({
start: 0,
displayed: PAGE_SIZE,
total: PAGE_SIZE + 1
})
};
var mockSecondPage = {
resources: [],
html: getResponseHtml({
start: PAGE_SIZE,
displayed: 1,
total: PAGE_SIZE + 1
})
};
var mockEmptyPage = {
resources: [],
html: getResponseHtml({
start: 0,
displayed: 0,
total: 0
})
};
var respondWithMockPage = function(requests) {
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);
};
var MockPagingView = PagedContainer.extend({
view: 'container_preview',
el: $("<div><div class='xblock' data-request-token='test_request_token'/></div>"),
model: new XBlockInfo({}, {parse: true})
});
describe("Paging Container", function() {
var pagingContainer;
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 });
});
describe("Container", function () {
describe("setPage", function () {
it('can set the current page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should not change page after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.setPage(1);
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
});
describe("nextPage", function () {
it('does not move forward after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.nextPage();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.nextPage();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can not move forward from the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.nextPage();
expect(requests.length).toBe(1);
});
});
describe("previousPage", function () {
it('can move back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.previousPage();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can not move back from the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.previousPage();
expect(requests.length).toBe(1);
});
it('does not move back after a server error', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.previousPage();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
});
});
describe("PagingHeader", function () {
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-header.underscore');
appendSetFixtures($("<script>", { id: "paging-header-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
pagingContainer.render();
});
it('does not move forward if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.next-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.next-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should be enabled when there is at least one more page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.next-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled on the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
});
it('should be disabled on an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.next-page-link')).toHaveClass('is-disabled');
});
});
describe("Previous page button", function () {
beforeEach(function () {
pagingContainer.render();
});
it('does not move back if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.previous-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can go back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingHeader.$('.previous-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('should be disabled on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
});
it('should be enabled on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled for an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.previous-page-link')).toHaveClass('is-disabled');
});
});
describe("Page metadata section", function() {
it('shows the correct metadata for the current page', function () {
var requests = AjaxHelpers.requests(this),
message;
pagingContainer.setPage(0);
respondWithMockPage(requests);
message = pagingContainer.pagingHeader.$('.meta').html().trim();
expect(message).toBe('<p>Showing <span class="count-current-shown">1-3</span>' +
' out of <span class="count-total">4 total</span>, ' +
'sorted by <span class="sort-order">Date added</span> descending</p>');
});
});
describe("Children count label", function () {
it('should show correct count on first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('1-3');
});
it('should show correct count on second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('4-4');
});
it('should show correct count for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.count-current-shown')).toHaveHtml('0-0');
});
});
describe("Children total label", function () {
it('should show correct total on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('4 total');
});
it('should show correct total on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('4 total');
});
it('should show zero total for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingHeader.$('.count-total')).toHaveText('0 total');
});
});
});
describe("PagingFooter", function () {
var pagingFooter;
beforeEach(function () {
var pagingFooterTpl = readFixtures('paging-footer.underscore');
appendSetFixtures($("<script>", { id: "paging-footer-tpl", type: "text/template" }).text(pagingFooterTpl));
});
describe("Next page button", function () {
beforeEach(function () {
// Render the page and header so that they can react to events
pagingContainer.render();
});
it('does not move forward if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.next-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('can move to the next page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.next-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('should be enabled when there is at least one more page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.next-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled on the final page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
});
it('should be disabled on an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.next-page-link')).toHaveClass('is-disabled');
});
});
describe("Previous page button", function () {
beforeEach(function () {
// Render the page and header so that they can react to events
pagingContainer.render();
});
it('does not move back if a server error occurs', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.previous-page-link').click();
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(1);
});
it('can go back a page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.previous-page-link').click();
respondWithMockPage(requests);
expect(pagingContainer.collection.currentPage).toBe(0);
});
it('should be disabled on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
});
it('should be enabled on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).not.toHaveClass('is-disabled');
});
it('should be disabled for an empty page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.previous-page-link')).toHaveClass('is-disabled');
});
});
describe("Current page label", function () {
it('should show 1 on the first page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('1');
});
it('should show 2 on the second page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(1);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('2');
});
it('should show 1 for an empty collection', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.current-page')).toHaveText('1');
});
});
describe("Page total label", function () {
it('should show the correct value with more than one page', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.total-pages')).toHaveText('2');
});
it('should show page 1 when there are no assets', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
AjaxHelpers.respondWithJson(requests, mockEmptyPage);
expect(pagingContainer.pagingFooter.$('.total-pages')).toHaveText('1');
});
});
describe("Page input field", function () {
var input;
it('should initially have a blank page input', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should handle invalid page requests', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('abc');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
expect(pagingContainer.collection.currentPage).toBe(0);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should switch pages via the input field', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('2');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
AjaxHelpers.respondWithJson(requests, mockSecondPage);
expect(pagingContainer.collection.currentPage).toBe(1);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
it('should handle AJAX failures when switching pages via the input field', function () {
var requests = AjaxHelpers.requests(this);
pagingContainer.setPage(0);
respondWithMockPage(requests);
pagingContainer.pagingFooter.$('.page-number-input').val('2');
pagingContainer.pagingFooter.$('.page-number-input').trigger('change');
requests[1].respond(500);
expect(pagingContainer.collection.currentPage).toBe(0);
expect(pagingContainer.pagingFooter.$('.page-number-input')).toHaveValue('');
});
});
});
});
});
...@@ -3,537 +3,560 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel ...@@ -3,537 +3,560 @@ define(["jquery", "underscore", "underscore.string", "js/common_helpers/ajax_hel
"js/views/pages/container", "js/models/xblock_info", "jquery.simulate"], "js/views/pages/container", "js/models/xblock_info", "jquery.simulate"],
function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, XBlockInfo) { function ($, _, str, AjaxHelpers, TemplateHelpers, EditHelpers, ContainerPage, XBlockInfo) {
describe("ContainerPage", function() { function parameterized_suite(label, global_page_options, fixtures) {
var lastRequest, renderContainerPage, expectComponents, respondWithHtml, describe(label + " ContainerPage", function () {
model, containerPage, requests, initialDisplayName, var lastRequest, getContainerPage, renderContainerPage, expectComponents, respondWithHtml,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'), model, containerPage, requests, initialDisplayName,
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'), mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'), mockContainerXBlockHtml = readFixtures(fixtures.initial),
mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'), mockXBlockHtml = readFixtures(fixtures.add_response),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'), mockBadContainerXBlockHtml = readFixtures('mock/mock-bad-javascript-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); mockBadXBlockContainerXBlockHtml = readFixtures('mock/mock-bad-xblock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
beforeEach(function () { mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
var newDisplayName = 'New Display Name';
EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
TemplateHelpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
EditHelpers.installMockXBlock({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
initialDisplayName = 'Test Container';
model = new XBlockInfo({
id: 'locator-container',
display_name: initialDisplayName,
category: 'vertical'
});
});
afterEach(function() { beforeEach(function () {
EditHelpers.uninstallMockXBlock(); var newDisplayName = 'New Display Name';
});
lastRequest = function() { return requests[requests.length - 1]; }; EditHelpers.installEditTemplates();
TemplateHelpers.installTemplate('xblock-string-field-editor');
respondWithHtml = function(html) { TemplateHelpers.installTemplate('container-message');
var requestIndex = requests.length - 1; appendSetFixtures(mockContainerPage);
AjaxHelpers.respondWithJson(
requests,
{ html: html, "resources": [] },
requestIndex
);
};
renderContainerPage = function(test, html, options) {
requests = AjaxHelpers.requests(test);
containerPage = new ContainerPage(_.extend(options || {}, {
model: model,
templates: EditHelpers.mockComponentTemplates,
el: $('#content')
}));
containerPage.render();
respondWithHtml(html);
};
expectComponents = function (container, locators) {
// verify expected components (in expected order) by their locators
var components = $(container).find('.studio-xblock-wrapper');
expect(components.length).toBe(locators.length);
_.each(locators, function(locator, locator_index) {
expect($(components[locator_index]).data('locator')).toBe(locator);
});
};
describe("Initial display", function() { EditHelpers.installMockXBlock({
it('can render itself', function() { data: "<p>Some HTML</p>",
renderContainerPage(this, mockContainerXBlockHtml); metadata: {
expect(containerPage.$('.xblock-header').length).toBe(9); display_name: newDisplayName
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); }
}); });
it('shows a loading indicator', function() {
requests = AjaxHelpers.requests(this);
containerPage.render();
expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
respondWithHtml(mockContainerXBlockHtml);
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('can show an xblock with broken JavaScript', function() {
renderContainerPage(this, mockBadContainerXBlockHtml);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('can show an xblock with an invalid XBlock', function() { initialDisplayName = 'Test Container';
renderContainerPage(this, mockBadXBlockContainerXBlockHtml);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
});
it('inline edits the display name when performing a new action', function() { model = new XBlockInfo({
renderContainerPage(this, mockContainerXBlockHtml, { id: 'locator-container',
action: 'new' display_name: initialDisplayName,
category: 'vertical'
}); });
expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.xblock-field-input')).not.toHaveClass('is-hidden');
}); });
});
describe("Editing the container", function() { afterEach(function () {
var updatedDisplayName = 'Updated Test Container', EditHelpers.uninstallMockXBlock();
getDisplayNameWrapper;
afterEach(function() {
EditHelpers.cancelModalIfShowing();
}); });
getDisplayNameWrapper = function() { lastRequest = function () {
return containerPage.$('.wrapper-xblock-field'); return requests[requests.length - 1];
}; };
it('can edit itself', function() { respondWithHtml = function (html) {
var editButtons, displayNameElement; var requestIndex = requests.length - 1;
renderContainerPage(this, mockContainerXBlockHtml); AjaxHelpers.respondWithJson(
displayNameElement = containerPage.$('.page-header-title'); requests,
{ html: html, "resources": [] },
// Click the root edit button requestIndex
editButtons = containerPage.$('.nav-actions .edit-button'); );
editButtons.first().click(); };
// Expect a request to be made to show the studio view for the container
expect(str.startsWith(lastRequest().url, '/xblock/locator-container/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockContainerXBlockHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
// Expect the correct title to be shown
expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container');
// Press the save button and respond with a success message to the save
EditHelpers.pressModalButton('.action-save');
AjaxHelpers.respondWithJson(requests, { });
expect(EditHelpers.isShowingModal()).toBeFalsy();
// Expect the last request be to refresh the container page
expect(str.startsWith(lastRequest().url,
'/xblock/locator-container/container_preview')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
// Respond to the subsequent xblock info fetch request.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
// Expect the title to have been updated
expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
});
it('can inline edit the display name', function() { getContainerPage = function (options) {
var displayNameInput, displayNameWrapper; var default_options = {
renderContainerPage(this, mockContainerXBlockHtml); model: model,
displayNameWrapper = getDisplayNameWrapper(); templates: EditHelpers.mockComponentTemplates,
displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName); el: $('#content')
displayNameInput.change(); };
// This is the response for the change operation. return new ContainerPage(_.extend(options || {}, global_page_options, default_options));
AjaxHelpers.respondWithJson(requests, { }); };
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
});
});
describe("Editing an xblock", function() { renderContainerPage = function (test, html, options) {
afterEach(function() { requests = AjaxHelpers.requests(test);
EditHelpers.cancelModalIfShowing(); containerPage = getContainerPage(options);
}); containerPage.render();
respondWithHtml(html);
};
it('can show an edit modal for a child xblock', function() { expectComponents = function (container, locators) {
var editButtons; // verify expected components (in expected order) by their locators
renderContainerPage(this, mockContainerXBlockHtml); var components = $(container).find('.studio-xblock-wrapper');
editButtons = containerPage.$('.wrapper-xblock .edit-button'); expect(components.length).toBe(locators.length);
// The container should have rendered six mock xblocks _.each(locators, function (locator, locator_index) {
expect(editButtons.length).toBe(6); expect($(components[locator_index]).data('locator')).toBe(locator);
editButtons[0].click();
// Make sure that the correct xblock is requested to be edited
expect(str.startsWith(lastRequest().url, '/xblock/locator-component-A1/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
}); });
expect(EditHelpers.isShowingModal()).toBeTruthy(); };
});
it('can show an edit modal for a child xblock with broken JavaScript', function() { describe("Initial display", function () {
var editButtons; it('can render itself', function () {
renderContainerPage(this, mockBadContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button'); expect(containerPage.$('.xblock-header').length).toBe(9);
editButtons[0].click(); expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
}); });
expect(EditHelpers.isShowingModal()).toBeTruthy();
});
});
describe("Editing an xmodule", function() {
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
beforeEach(function () { it('shows a loading indicator', function () {
EditHelpers.installMockXModule({ requests = AjaxHelpers.requests(this);
data: "<p>Some HTML</p>", containerPage = getContainerPage();
metadata: { containerPage.render();
display_name: newDisplayName expect(containerPage.$('.ui-loading')).not.toHaveClass('is-hidden');
} respondWithHtml(mockContainerXBlockHtml);
expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
}); });
});
afterEach(function() { it('can show an xblock with broken JavaScript', function () {
EditHelpers.uninstallMockXModule(); renderContainerPage(this, mockBadContainerXBlockHtml);
EditHelpers.cancelModalIfShowing(); expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
}); expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
it('can save changes to settings', function() {
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons[0].click();
AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditor,
resources: []
}); });
modal = $('.edit-xblock-modal'); it('can show an xblock with an invalid XBlock', function () {
expect(modal.length).toBe(1); renderContainerPage(this, mockBadXBlockContainerXBlockHtml);
// Click on the settings tab expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
modal.find('.settings-button').click(); expect(containerPage.$('.ui-loading')).toHaveClass('is-hidden');
// Change the display name's text
modal.find('.setting-input').text("Mock Update");
// Press the save button
modal.find('.action-save').click();
// Respond to the save
AjaxHelpers.respondWithJson(requests, {
id: model.id
}); });
// Respond to the request to refresh it('inline edits the display name when performing a new action', function () {
respondWithHtml(mockUpdatedXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml, {
action: 'new'
// Verify that the xblock was updated });
expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update'); expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
expect(containerPage.$('.xblock-field-input')).not.toHaveClass('is-hidden');
});
}); });
});
describe("xblock operations", function() {
var getGroupElement,
NUM_COMPONENTS_PER_GROUP = 3, GROUP_TO_TEST = "A",
allComponentsInGroup = _.map(
_.range(NUM_COMPONENTS_PER_GROUP),
function(index) { return 'locator-component-' + GROUP_TO_TEST + (index + 1); }
);
getGroupElement = function() {
return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']");
};
describe("Deleting an xblock", function() { describe("Editing the container", function () {
var clickDelete, deleteComponent, deleteComponentWithSuccess, var updatedDisplayName = 'Updated Test Container',
promptSpy; getDisplayNameWrapper;
beforeEach(function() { afterEach(function () {
promptSpy = EditHelpers.createPromptSpy(); EditHelpers.cancelModalIfShowing();
}); });
clickDelete = function(componentIndex, clickNo) { getDisplayNameWrapper = function () {
return containerPage.$('.wrapper-xblock-field');
};
// find all delete buttons for the given group it('can edit itself', function () {
var deleteButtons = getGroupElement().find(".delete-button"); var editButtons, displayNameElement;
expect(deleteButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); renderContainerPage(this, mockContainerXBlockHtml);
displayNameElement = containerPage.$('.page-header-title');
// click the requested delete button // Click the root edit button
deleteButtons[componentIndex].click(); editButtons = containerPage.$('.nav-actions .edit-button');
editButtons.first().click();
// click the 'yes' or 'no' button in the prompt // Expect a request to be made to show the studio view for the container
EditHelpers.confirmPrompt(promptSpy, clickNo); expect(str.startsWith(lastRequest().url, '/xblock/locator-container/studio_view')).toBeTruthy();
}; AjaxHelpers.respondWithJson(requests, {
html: mockContainerXBlockHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
deleteComponent = function(componentIndex) { // Expect the correct title to be shown
clickDelete(componentIndex); expect(EditHelpers.getModalTitle()).toBe('Editing: Test Container');
AjaxHelpers.respondWithJson(requests, {});
// second to last request contains given component's id (to delete the component) // Press the save button and respond with a success message to the save
AjaxHelpers.expectJsonRequest(requests, 'DELETE', EditHelpers.pressModalButton('.action-save');
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1), AjaxHelpers.respondWithJson(requests, { });
null, requests.length - 2); expect(EditHelpers.isShowingModal()).toBeFalsy();
// final request to refresh the xblock info // Expect the last request be to refresh the container page
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); expect(str.startsWith(lastRequest().url,
}; '/xblock/locator-container/container_preview')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
deleteComponentWithSuccess = function(componentIndex) { // Respond to the subsequent xblock info fetch request.
deleteComponent(componentIndex); AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
// verify the new list of components within the group // Expect the title to have been updated
expectComponents( expect(displayNameElement.text().trim()).toBe(updatedDisplayName);
getGroupElement(), });
_.without(allComponentsInGroup, allComponentsInGroup[componentIndex])
);
};
it("can delete the first xblock", function() { it('can inline edit the display name', function () {
var displayNameInput, displayNameWrapper;
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(0); displayNameWrapper = getDisplayNameWrapper();
displayNameInput = EditHelpers.inlineEdit(displayNameWrapper, updatedDisplayName);
displayNameInput.change();
// This is the response for the change operation.
AjaxHelpers.respondWithJson(requests, { });
// This is the response for the subsequent fetch operation.
AjaxHelpers.respondWithJson(requests, {"display_name": updatedDisplayName});
EditHelpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
expect(containerPage.model.get('display_name')).toBe(updatedDisplayName);
}); });
});
it("can delete a middle xblock", function() { describe("Editing an xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml); afterEach(function () {
deleteComponentWithSuccess(1); EditHelpers.cancelModalIfShowing();
}); });
it("can delete the last xblock", function() { it('can show an edit modal for a child xblock', function () {
var editButtons;
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1); editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons[0].click();
// Make sure that the correct xblock is requested to be edited
expect(str.startsWith(lastRequest().url, '/xblock/locator-component-A1/studio_view')).toBeTruthy();
AjaxHelpers.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(EditHelpers.isShowingModal()).toBeTruthy();
}); });
it("can delete an xblock with broken JavaScript", function() { it('can show an edit modal for a child xblock with broken JavaScript', function () {
var editButtons;
renderContainerPage(this, mockBadContainerXBlockHtml); renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.delete-button').first().click(); editButtons = containerPage.$('.wrapper-xblock .edit-button');
EditHelpers.confirmPrompt(promptSpy); editButtons[0].click();
AjaxHelpers.respondWithJson(requests, {}); AjaxHelpers.respondWithJson(requests, {
// expect the second to last request to be a delete of the xblock html: mockXBlockEditorHtml,
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript', resources: []
null, requests.length - 2); });
// expect the last request to be a fetch of the xblock info for the parent container expect(EditHelpers.isShowingModal()).toBeTruthy();
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
}); });
});
it('does not delete when clicking No in prompt', function () { describe("Editing an xmodule", function () {
var numRequests; var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
renderContainerPage(this, mockContainerXBlockHtml);
numRequests = requests.length;
// click delete on the first component but press no
clickDelete(0, true);
// all components should still exist
expectComponents(getGroupElement(), allComponentsInGroup);
// no requests should have been sent to the server beforeEach(function () {
expect(requests.length).toBe(numRequests); EditHelpers.installMockXModule({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
}); });
it('shows a notification during the delete operation', function() { afterEach(function () {
var notificationSpy = EditHelpers.createNotificationSpy(); EditHelpers.uninstallMockXModule();
renderContainerPage(this, mockContainerXBlockHtml); EditHelpers.cancelModalIfShowing();
clickDelete(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
AjaxHelpers.respondWithJson(requests, {});
EditHelpers.verifyNotificationHidden(notificationSpy);
}); });
it('does not delete an xblock upon failure', function () { it('can save changes to settings', function () {
var notificationSpy = EditHelpers.createNotificationSpy(); var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
clickDelete(0); editButtons = containerPage.$('.wrapper-xblock .edit-button');
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); // The container should have rendered six mock xblocks
AjaxHelpers.respondWithError(requests); expect(editButtons.length).toBe(6);
EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); editButtons[0].click();
expectComponents(getGroupElement(), allComponentsInGroup); AjaxHelpers.respondWithJson(requests, {
html: mockXModuleEditor,
resources: []
});
modal = $('.edit-xblock-modal');
expect(modal.length).toBe(1);
// Click on the settings tab
modal.find('.settings-button').click();
// Change the display name's text
modal.find('.setting-input').text("Mock Update");
// Press the save button
modal.find('.action-save').click();
// Respond to the save
AjaxHelpers.respondWithJson(requests, {
id: model.id
});
// Respond to the request to refresh
respondWithHtml(mockUpdatedXBlockHtml);
// Verify that the xblock was updated
expect(containerPage.$('.mock-updated-content').text()).toBe('Mock Update');
}); });
}); });
describe("Duplicating an xblock", function() { describe("xblock operations", function () {
var clickDuplicate, duplicateComponentWithSuccess, var getGroupElement,
refreshXBlockSpies; NUM_COMPONENTS_PER_GROUP = 3, GROUP_TO_TEST = "A",
allComponentsInGroup = _.map(
_.range(NUM_COMPONENTS_PER_GROUP),
function (index) {
return 'locator-component-' + GROUP_TO_TEST + (index + 1);
}
);
clickDuplicate = function(componentIndex) { getGroupElement = function () {
return containerPage.$("[data-locator='locator-group-" + GROUP_TO_TEST + "']");
};
// find all duplicate buttons for the given group describe("Deleting an xblock", function () {
var duplicateButtons = getGroupElement().find(".duplicate-button"); var clickDelete, deleteComponent, deleteComponentWithSuccess,
expect(duplicateButtons.length).toBe(NUM_COMPONENTS_PER_GROUP); promptSpy;
// click the requested duplicate button beforeEach(function () {
duplicateButtons[componentIndex].click(); promptSpy = EditHelpers.createPromptSpy();
}; });
clickDelete = function (componentIndex, clickNo) {
duplicateComponentWithSuccess = function(componentIndex) { // find all delete buttons for the given group
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock"); var deleteButtons = getGroupElement().find(".delete-button");
expect(deleteButtons.length).toBe(NUM_COMPONENTS_PER_GROUP);
clickDuplicate(componentIndex); // click the requested delete button
deleteButtons[componentIndex].click();
// verify content of request // click the 'yes' or 'no' button in the prompt
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { EditHelpers.confirmPrompt(promptSpy, clickNo);
'duplicate_source_locator': 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1), };
'parent_locator': 'locator-group-' + GROUP_TO_TEST
deleteComponent = function (componentIndex) {
clickDelete(componentIndex);
AjaxHelpers.respondWithJson(requests, {});
// second to last request contains given component's id (to delete the component)
AjaxHelpers.expectJsonRequest(requests, 'DELETE',
'/xblock/locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
null, requests.length - 2);
// final request to refresh the xblock info
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
};
deleteComponentWithSuccess = function (componentIndex) {
deleteComponent(componentIndex);
// verify the new list of components within the group
expectComponents(
getGroupElement(),
_.without(allComponentsInGroup, allComponentsInGroup[componentIndex])
);
};
it("can delete the first xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(0);
}); });
// send the response it("can delete a middle xblock", function () {
AjaxHelpers.respondWithJson(requests, { renderContainerPage(this, mockContainerXBlockHtml);
'locator': 'locator-duplicated-component' deleteComponentWithSuccess(1);
}); });
// expect parent container to be refreshed it("can delete the last xblock", function () {
expect(refreshXBlockSpies).toHaveBeenCalled(); renderContainerPage(this, mockContainerXBlockHtml);
}; deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can duplicate the first xblock", function() { it("can delete an xblock with broken JavaScript", function () {
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockBadContainerXBlockHtml);
duplicateComponentWithSuccess(0); containerPage.$('.delete-button').first().click();
}); EditHelpers.confirmPrompt(promptSpy);
AjaxHelpers.respondWithJson(requests, {});
// expect the second to last request to be a delete of the xblock
AjaxHelpers.expectJsonRequest(requests, 'DELETE', '/xblock/locator-broken-javascript',
null, requests.length - 2);
// expect the last request to be a fetch of the xblock info for the parent container
AjaxHelpers.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
});
it("can duplicate a middle xblock", function() { it('does not delete when clicking No in prompt', function () {
renderContainerPage(this, mockContainerXBlockHtml); var numRequests;
duplicateComponentWithSuccess(1);
});
it("can duplicate the last xblock", function() { renderContainerPage(this, mockContainerXBlockHtml);
renderContainerPage(this, mockContainerXBlockHtml); numRequests = requests.length;
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can duplicate an xblock with broken JavaScript", function() { // click delete on the first component but press no
renderContainerPage(this, mockBadContainerXBlockHtml); clickDelete(0, true);
containerPage.$('.duplicate-button').first().click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', { // all components should still exist
'duplicate_source_locator': 'locator-broken-javascript', expectComponents(getGroupElement(), allComponentsInGroup);
'parent_locator': 'locator-container'
// no requests should have been sent to the server
expect(requests.length).toBe(numRequests);
}); });
});
it('shows a notification when duplicating', function () { it('shows a notification during the delete operation', function () {
var notificationSpy = EditHelpers.createNotificationSpy(); var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
clickDuplicate(0); clickDelete(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
AjaxHelpers.respondWithJson(requests, {"locator": "new_item"}); AjaxHelpers.respondWithJson(requests, {});
EditHelpers.verifyNotificationHidden(notificationSpy); EditHelpers.verifyNotificationHidden(notificationSpy);
}); });
it('does not duplicate an xblock upon failure', function () { it('does not delete an xblock upon failure', function () {
var notificationSpy = EditHelpers.createNotificationSpy(); var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml); renderContainerPage(this, mockContainerXBlockHtml);
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock"); clickDelete(0);
clickDuplicate(0); EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/); AjaxHelpers.respondWithError(requests);
AjaxHelpers.respondWithError(requests); EditHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
expectComponents(getGroupElement(), allComponentsInGroup); expectComponents(getGroupElement(), allComponentsInGroup);
expect(refreshXBlockSpies).not.toHaveBeenCalled(); });
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
}); });
});
describe('createNewComponent ', function () { describe("Duplicating an xblock", function () {
var clickNewComponent; var clickDuplicate, duplicateComponentWithSuccess,
refreshXBlockSpies;
clickNewComponent = function (index) { clickDuplicate = function (componentIndex) {
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
};
it('sends the correct JSON to the server', function () { // find all duplicate buttons for the given group
renderContainerPage(this, mockContainerXBlockHtml); var duplicateButtons = getGroupElement().find(".duplicate-button");
clickNewComponent(0); expect(duplicateButtons.length).toBe(NUM_COMPONENTS_PER_GROUP);
EditHelpers.verifyXBlockRequest(requests, {
"category": "discussion",
"type": "discussion",
"parent_locator": "locator-group-A"
});
});
it('shows a notification while creating', function () { // click the requested duplicate button
var notificationSpy = EditHelpers.createNotificationSpy(); duplicateButtons[componentIndex].click();
renderContainerPage(this, mockContainerXBlockHtml); };
clickNewComponent(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/);
AjaxHelpers.respondWithJson(requests, { });
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not insert component upon failure', function () { duplicateComponentWithSuccess = function (componentIndex) {
var requestCount; refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0); clickDuplicate(componentIndex);
requestCount = requests.length;
AjaxHelpers.respondWithError(requests); // verify content of request
// No new requests should be made to refresh the view AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
expect(requests.length).toBe(requestCount); 'duplicate_source_locator': 'locator-component-' + GROUP_TO_TEST + (componentIndex + 1),
expectComponents(getGroupElement(), allComponentsInGroup); 'parent_locator': 'locator-group-' + GROUP_TO_TEST
}); });
describe('Template Picker', function() { // send the response
var showTemplatePicker, verifyCreateHtmlComponent, AjaxHelpers.respondWithJson(requests, {
mockXBlockHtml = readFixtures('mock/mock-xblock.underscore'); 'locator': 'locator-duplicated-component'
});
showTemplatePicker = function() { // expect parent container to be refreshed
containerPage.$('.new-component .new-component-type a.multiple-templates')[0].click(); expect(refreshXBlockSpies).toHaveBeenCalled();
}; };
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) { it("can duplicate the first xblock", function () {
var xblockCount; renderContainerPage(this, mockContainerXBlockHtml);
renderContainerPage(test, mockContainerXBlockHtml); duplicateComponentWithSuccess(0);
showTemplatePicker(); });
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html a')[templateIndex].click(); it("can duplicate a middle xblock", function () {
EditHelpers.verifyXBlockRequest(requests, expectedRequest); renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(1);
});
it("can duplicate the last xblock", function () {
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it("can duplicate an xblock with broken JavaScript", function () {
renderContainerPage(this, mockBadContainerXBlockHtml);
containerPage.$('.duplicate-button').first().click();
AjaxHelpers.expectJsonRequest(requests, 'POST', '/xblock/', {
'duplicate_source_locator': 'locator-broken-javascript',
'parent_locator': 'locator-container'
});
});
it('shows a notification when duplicating', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
clickDuplicate(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithJson(requests, {"locator": "new_item"}); AjaxHelpers.respondWithJson(requests, {"locator": "new_item"});
respondWithHtml(mockXBlockHtml); EditHelpers.verifyNotificationHidden(notificationSpy);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1); });
it('does not duplicate an xblock upon failure', function () {
var notificationSpy = EditHelpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
clickDuplicate(0);
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
AjaxHelpers.respondWithError(requests);
expectComponents(getGroupElement(), allComponentsInGroup);
expect(refreshXBlockSpies).not.toHaveBeenCalled();
EditHelpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
});
});
describe('createNewComponent ', function () {
var clickNewComponent;
clickNewComponent = function (index) {
containerPage.$(".new-component .new-component-type a.single-template")[index].click();
}; };
it('can add an HTML component without a template', function() { it('sends the correct JSON to the server', function () {
verifyCreateHtmlComponent(this, 0, { renderContainerPage(this, mockContainerXBlockHtml);
"category": "html", clickNewComponent(0);
EditHelpers.verifyXBlockRequest(requests, {
"category": "discussion",
"type": "discussion",
"parent_locator": "locator-group-A" "parent_locator": "locator-group-A"
}); });
}); });
it('can add an HTML component with a template', function() { it('shows a notification while creating', function () {
verifyCreateHtmlComponent(this, 1, { var notificationSpy = EditHelpers.createNotificationSpy();
"category": "html", renderContainerPage(this, mockContainerXBlockHtml);
"boilerplate" : "announcement.yaml", clickNewComponent(0);
"parent_locator": "locator-group-A" EditHelpers.verifyNotificationShowing(notificationSpy, /Adding/);
AjaxHelpers.respondWithJson(requests, { });
EditHelpers.verifyNotificationHidden(notificationSpy);
});
it('does not insert component upon failure', function () {
var requestCount;
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
requestCount = requests.length;
AjaxHelpers.respondWithError(requests);
// No new requests should be made to refresh the view
expect(requests.length).toBe(requestCount);
expectComponents(getGroupElement(), allComponentsInGroup);
});
describe('Template Picker', function () {
var showTemplatePicker, verifyCreateHtmlComponent;
showTemplatePicker = function () {
containerPage.$('.new-component .new-component-type a.multiple-templates')[0].click();
};
verifyCreateHtmlComponent = function (test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html a')[templateIndex].click();
EditHelpers.verifyXBlockRequest(requests, expectedRequest);
AjaxHelpers.respondWithJson(requests, {"locator": "new_item"});
respondWithHtml(mockXBlockHtml);
expect(containerPage.$('.studio-xblock-wrapper').length).toBe(xblockCount + 1);
};
it('can add an HTML component without a template', function () {
verifyCreateHtmlComponent(this, 0, {
"category": "html",
"parent_locator": "locator-group-A"
});
});
it('can add an HTML component with a template', function () {
verifyCreateHtmlComponent(this, 1, {
"category": "html",
"boilerplate": "announcement.yaml",
"parent_locator": "locator-group-A"
});
}); });
}); });
}); });
}); });
}); });
}); }
parameterized_suite("Non paged",
{ enable_paging: false },
{ initial: 'mock/mock-container-xblock.underscore', add_response: 'mock/mock-xblock.underscore' }
);
parameterized_suite("Paged",
{ enable_paging: true, page_size: 42 },
{
initial: 'mock/mock-container-paged-xblock.underscore',
add_response: 'mock/mock-container-paged-after-add-xblock.underscore'
});
}); });
...@@ -123,6 +123,10 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", ...@@ -123,6 +123,10 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
}); });
}, },
acknowledgeXBlockDeletion: function(locator){
this.notifyRuntime('deleted-child', locator);
},
refresh: function() { refresh: function() {
var sortableInitializedClass = this.makeRequestSpecificSelector('.reorderable-container.ui-sortable'); var sortableInitializedClass = this.makeRequestSpecificSelector('.reorderable-container.ui-sortable');
this.$(sortableInitializedClass).sortable('refresh'); this.$(sortableInitializedClass).sortable('refresh');
......
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification",
"js/views/paging_header", "js/views/paging_footer"],
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView, PagingHeader, PagingFooter) {
var LibraryContainerView = XBlockView.extend({
// Store the request token of the first xblock on the page (which we know was rendered by Studio when
// the page was generated). Use that request token to filter out user-defined HTML in any
// child xblocks within the page.
requestToken: "",
initialize: function(options){
var self = this;
XBlockView.prototype.initialize.call(this);
this.page_size = this.options.page_size || 10;
if (options) {
this.page_reload_callback = options.page_reload_callback;
}
// emulating Backbone.paginator interface
this.collection = {
currentPage: 0,
totalPages: 0,
totalCount: 0,
sortDirection: "desc",
start: 0,
_size: 0,
bind: function() {}, // no-op
size: function() { return self.collection._size; }
};
},
render: function(options) {
var eff_options = options || {};
if (eff_options.block_added) {
this.collection.currentPage = this.getPageCount(this.collection.totalCount+1) - 1;
}
eff_options.page_number = typeof eff_options.page_number !== "undefined"
? eff_options.page_number
: this.collection.currentPage;
return this.renderPage(eff_options);
},
renderPage: function(options){
var self = this,
view = this.view,
xblockInfo = this.model,
xblockUrl = xblockInfo.url();
return $.ajax({
url: decodeURIComponent(xblockUrl) + "/" + view,
type: 'GET',
cache: false,
data: this.getRenderParameters(options.page_number),
headers: { Accept: 'application/json' },
success: function(fragment) {
self.handleXBlockFragment(fragment, options);
self.processPaging({ requested_page: options.page_number });
if (options.paging && self.page_reload_callback){
self.page_reload_callback(self.$el);
}
}
});
},
getRenderParameters: function(page_number) {
return {
enable_paging: true,
page_size: this.page_size,
page_number: page_number
};
},
getPageCount: function(total_count){
if (total_count==0) return 1;
return Math.ceil(total_count / this.page_size);
},
setPage: function(page_number) {
this.render({ page_number: page_number, paging: true });
},
nextPage: function() {
var collection = this.collection,
currentPage = collection.currentPage,
lastPage = collection.totalPages - 1;
if (currentPage < lastPage) {
this.setPage(currentPage + 1);
}
},
previousPage: function() {
var collection = this.collection,
currentPage = collection.currentPage;
if (currentPage > 0) {
this.setPage(currentPage - 1);
}
},
processPaging: function(options){
var $element = this.$el.find('.xblock-container-paging-parameters'),
total = $element.data('total'),
displayed = $element.data('displayed'),
start = $element.data('start');
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.processPagingHeaderAndFooter();
},
processPagingHeaderAndFooter: function(){
if (this.pagingHeader)
this.pagingHeader.undelegateEvents();
if (this.pagingFooter)
this.pagingFooter.undelegateEvents();
this.pagingHeader = new PagingHeader({
view: this,
el: this.$el.find('.container-paging-header')
});
this.pagingFooter = new PagingFooter({
view: this,
el: this.$el.find('.container-paging-footer')
});
this.pagingHeader.render();
this.pagingFooter.render();
},
xblockReady: function () {
XBlockView.prototype.xblockReady.call(this);
this.requestToken = this.$('div.xblock').first().data('request-token');
},
refresh: function() { },
acknowledgeXBlockDeletion: function (locator){
this.notifyRuntime('deleted-child', locator);
this.collection._size -= 1;
this.collection.totalCount -= 1;
// pages are counted from 0 - thus currentPage == 1 if we're on second page
if (this.collection._size == 0 && this.collection.currentPage >= 1) {
this.setPage(this.collection.currentPage - 1);
this.collection.totalPages -= 1;
}
else {
this.pagingHeader.render();
this.pagingFooter.render();
}
},
makeRequestSpecificSelector: function(selector) {
return 'div.xblock[data-request-token="' + this.requestToken + '"] > ' + selector;
},
sortDisplayName: function() {
return "Date added"; // TODO add support for sorting
}
});
return LibraryContainerView;
}); // end define();
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
* This page allows the user to understand and manipulate the xblock and its children. * This page allows the user to understand and manipulate the xblock and its children.
*/ */
define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/view_utils", define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views/utils/view_utils",
"js/views/container", "js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock", "js/views/container", "js/views/library_container", "js/views/xblock", "js/views/components/add_xblock", "js/views/modals/edit_xblock",
"js/models/xblock_info", "js/views/xblock_string_field_editor", "js/views/pages/container_subviews", "js/models/xblock_info", "js/views/xblock_string_field_editor", "js/views/pages/container_subviews",
"js/views/unit_outline", "js/views/utils/xblock_utils"], "js/views/unit_outline", "js/views/utils/xblock_utils"],
function ($, _, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent, function ($, _, gettext, BasePage, ViewUtils, ContainerView, PagedContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView, EditXBlockModal, XBlockInfo, XBlockStringFieldEditor, ContainerSubviews, UnitOutlineView,
XBlockUtils) { XBlockUtils) {
'use strict'; 'use strict';
...@@ -27,6 +27,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -27,6 +27,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
initialize: function(options) { initialize: function(options) {
BasePage.prototype.initialize.call(this, options); BasePage.prototype.initialize.call(this, options);
this.enable_paging = options.enable_paging || false;
if (this.enable_paging) {
this.page_size = options.page_size || 10;
}
this.nameEditor = new XBlockStringFieldEditor({ this.nameEditor = new XBlockStringFieldEditor({
el: this.$('.wrapper-xblock-field'), el: this.$('.wrapper-xblock-field'),
model: this.model model: this.model
...@@ -35,11 +39,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -35,11 +39,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
if (this.options.action === 'new') { if (this.options.action === 'new') {
this.nameEditor.$('.xblock-field-value-edit').click(); this.nameEditor.$('.xblock-field-value-edit').click();
} }
this.xblockView = new ContainerView({ this.xblockView = this.getXBlockView();
el: this.$('.wrapper-xblock'),
model: this.model,
view: this.view
});
this.messageView = new ContainerSubviews.MessageView({ this.messageView = new ContainerSubviews.MessageView({
el: this.$('.container-message'), el: this.$('.container-message'),
model: this.model model: this.model
...@@ -75,6 +75,28 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -75,6 +75,28 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
} }
}, },
getXBlockView: function(){
var self = this,
parameters = {
el: this.$('.wrapper-xblock'),
model: this.model,
view: this.view
};
if (this.enable_paging) {
parameters = _.extend(parameters, {
page_size: this.page_size,
page_reload_callback: function($element) {
self.renderAddXBlockComponents();
}
});
return new PagedContainerView(parameters);
}
else {
return new ContainerView(parameters);
}
},
render: function(options) { render: function(options) {
var self = this, var self = this,
xblockView = this.xblockView, xblockView = this.xblockView,
...@@ -106,7 +128,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -106,7 +128,8 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Re-enable Backbone events for any updated DOM elements // Re-enable Backbone events for any updated DOM elements
self.delegateEvents(); self.delegateEvents();
} },
block_added: options && options.block_added
}); });
}, },
...@@ -144,7 +167,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -144,7 +167,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
modal.edit(xblockElement, this.model, { modal.edit(xblockElement, this.model, {
refresh: function() { refresh: function() {
self.refreshXBlock(xblockElement); self.refreshXBlock(xblockElement, false);
} }
}); });
}, },
...@@ -226,7 +249,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -226,7 +249,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// Inform the runtime that the child has been deleted in case // Inform the runtime that the child has been deleted in case
// other views are listening to deletion events. // other views are listening to deletion events.
xblockView.notifyRuntime('deleted-child', parent.data('locator')); xblockView.acknowledgeXBlockDeletion(parent.data('locator'));
// Update publish and last modified information from the server. // Update publish and last modified information from the server.
this.model.fetch(); this.model.fetch();
...@@ -235,7 +258,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -235,7 +258,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
onNewXBlock: function(xblockElement, scrollOffset, data) { onNewXBlock: function(xblockElement, scrollOffset, data) {
ViewUtils.setScrollOffset(xblockElement, scrollOffset); ViewUtils.setScrollOffset(xblockElement, scrollOffset);
xblockElement.data('locator', data.locator); xblockElement.data('locator', data.locator);
return this.refreshXBlock(xblockElement); return this.refreshXBlock(xblockElement, true);
}, },
/** /**
...@@ -243,17 +266,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -243,17 +266,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
* reorderable container then the element will be refreshed inline. If not, then the * reorderable container then the element will be refreshed inline. If not, then the
* parent container will be refreshed instead. * parent container will be refreshed instead.
* @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.
*/ */
refreshXBlock: function(element) { refreshXBlock: function(element, block_added) {
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}); this.render({refresh: true, block_added: block_added});
} else if (parentElement.hasClass('reorderable-container')) { } else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement); this.refreshChildXBlock(xblockElement);
} else { } else {
this.refreshXBlock(this.findXBlockElement(parentElement)); this.refreshXBlock(this.findXBlockElement(parentElement), block_added);
} }
}, },
......
...@@ -38,6 +38,12 @@ define(["underscore", "js/views/baseview"], function(_, BaseView) { ...@@ -38,6 +38,12 @@ define(["underscore", "js/views/baseview"], function(_, BaseView) {
currentPage = collection.currentPage + 1, currentPage = collection.currentPage + 1,
pageInput = this.$("#page-number-input"), pageInput = this.$("#page-number-input"),
pageNumber = parseInt(pageInput.val(), 10); pageNumber = parseInt(pageInput.val(), 10);
if (pageNumber > collection.totalPages) {
pageNumber = false;
}
if (pageNumber <= 0) {
pageNumber = false;
}
if (pageNumber && pageNumber !== currentPage) { if (pageNumber && pageNumber !== currentPage) {
view.setPage(pageNumber - 1); view.setPage(pageNumber - 1);
} }
......
// studio - elements - pagination
// ==========================
%pagination {
@include clearfix;
display: inline-block;
width: flex-grid(3, 12);
&.pagination-compact {
@include text-align(right);
}
&.pagination-full {
display: block;
width: flex-grid(4, 12);
margin: $baseline auto;
}
.nav-item {
position: relative;
display: inline-block;
}
.nav-link {
@include transition(all $tmg-f2 ease-in-out 0s);
display: block;
padding: ($baseline/4) ($baseline*0.75);
&.previous {
margin-right: ($baseline/2);
}
&.next {
margin-left: ($baseline/2);
}
&:hover {
background-color: $blue;
border-radius: 3px;
color: $white;
}
&.is-disabled {
background-color: transparent;
color: $gray-l2;
pointer-events: none;
}
}
.nav-label {
@extend .sr;
}
.pagination-form,
.current-page,
.page-divider,
.total-pages {
display: inline-block;
}
.current-page,
.page-number-input,
.total-pages {
@extend %t-copy-base;
@extend %t-strong;
width: ($baseline*2.5);
margin: 0 ($baseline*0.75);
padding: ($baseline/4);
text-align: center;
color: $gray;
}
.current-page {
@extend %ui-depth1;
position: absolute;
@include left(-($baseline/4));
}
.page-divider {
@extend %t-title4;
@extend %t-regular;
vertical-align: middle;
color: $gray-l2;
}
.pagination-form {
@extend %ui-depth2;
position: relative;
.page-number-label,
.submit-pagination-form {
@extend .sr;
}
.page-number-input {
@include transition(all $tmg-f2 ease-in-out 0s);
border: 1px solid transparent;
border-bottom: 1px dotted $gray-l2;
border-radius: 0;
box-shadow: none;
background: none;
&:hover {
background-color: $white;
opacity: 0.6;
}
&:focus {
// borrowing the base input focus styles to match overall app
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
opacity: 1.0;
box-shadow: 0 0 3px $shadow-d1 inset;
background-color: $white;
border: 1px solid transparent;
border-radius: 3px;
}
}
}
}
\ No newline at end of file
...@@ -28,120 +28,7 @@ ...@@ -28,120 +28,7 @@
} }
.pagination { .pagination {
@include clearfix; @extend %pagination;
display: inline-block;
width: flex-grid(3, 12);
&.pagination-compact {
@include text-align(right);
}
&.pagination-full {
display: block;
width: flex-grid(4, 12);
margin: $baseline auto;
}
.nav-item {
position: relative;
display: inline-block;
}
.nav-link {
@include transition(all $tmg-f2 ease-in-out 0s);
display: block;
padding: ($baseline/4) ($baseline*0.75);
&.previous {
margin-right: ($baseline/2);
}
&.next {
margin-left: ($baseline/2);
}
&:hover {
background-color: $blue;
border-radius: 3px;
color: $white;
}
&.is-disabled {
background-color: transparent;
color: $gray-l2;
pointer-events: none;
}
}
.nav-label {
@extend .sr;
}
.pagination-form,
.current-page,
.page-divider,
.total-pages {
display: inline-block;
}
.current-page,
.page-number-input,
.total-pages {
@extend %t-copy-base;
@extend %t-strong;
width: ($baseline*2.5);
margin: 0 ($baseline*0.75);
padding: ($baseline/4);
text-align: center;
color: $gray;
}
.current-page {
@extend %ui-depth1;
position: absolute;
@include left(-($baseline/4));
}
.page-divider {
@extend %t-title4;
@extend %t-regular;
vertical-align: middle;
color: $gray-l2;
}
.pagination-form {
@extend %ui-depth2;
position: relative;
.page-number-label,
.submit-pagination-form {
@extend .sr;
}
.page-number-input {
@include transition(all $tmg-f2 ease-in-out 0s);
border: 1px solid transparent;
border-bottom: 1px dotted $gray-l2;
border-radius: 0;
box-shadow: none;
background: none;
&:hover {
background-color: $white;
opacity: 0.6;
}
&:focus {
// borrowing the base input focus styles to match overall app
@include linear-gradient($paleYellow, tint($paleYellow, 90%));
opacity: 1.0;
box-shadow: 0 0 3px $shadow-d1 inset;
background-color: $white;
border: 1px solid transparent;
border-radius: 3px;
}
}
}
} }
.assets-table { .assets-table {
......
...@@ -103,6 +103,37 @@ ...@@ -103,6 +103,37 @@
} }
} }
.container-paging-header {
.meta-wrap {
margin: $baseline $baseline/2;
}
.meta {
@extend %t-copy-sub2;
display: inline-block;
vertical-align: top;
width: flex-grid(9, 12);
color: $gray-l1;
.count-current-shown,
.count-total,
.sort-order {
@extend %t-strong;
}
}
.pagination {
@extend %pagination;
}
}
.container-paging-footer {
.pagination {
@extend %pagination;
}
}
// ==================== // ====================
//UI: default internal xblock content styles //UI: default internal xblock content styles
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
// +Base - Elements // +Base - Elements
// ==================== // ====================
@import 'elements/typography'; @import 'elements/typography';
@import 'elements/pagination'; // pagination
@import 'elements/icons'; // references to icons used @import 'elements/icons'; // references to icons used
@import 'elements/controls'; // buttons, link styles, sliders, etc. @import 'elements/controls'; // buttons, link styles, sliders, etc.
@import 'elements/xblocks'; // studio rendering chrome for xblocks @import 'elements/xblocks'; // studio rendering chrome for xblocks
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
// +Base - Elements // +Base - Elements
// ==================== // ====================
@import 'elements/typography'; @import 'elements/typography';
@import 'elements/pagination'; // pagination
@import 'elements/icons'; // references to icons used @import 'elements/icons'; // references to icons used
@import 'elements/controls'; // buttons, link styles, sliders, etc. @import 'elements/controls'; // buttons, link styles, sliders, etc.
@import 'elements/xblocks'; // studio rendering chrome for xblocks @import 'elements/xblocks'; // studio rendering chrome for xblocks
......
...@@ -31,7 +31,10 @@ from django.utils.translation import ugettext as _ ...@@ -31,7 +31,10 @@ from django.utils.translation import ugettext as _
require(["js/factories/container"], function(ContainerFactory) { require(["js/factories/container"], function(ContainerFactory) {
ContainerFactory( ContainerFactory(
${component_templates | n}, ${json.dumps(xblock_info) | n}, ${component_templates | n}, ${json.dumps(xblock_info) | n},
"${action}", ${json.dumps(is_unit_page)} "${action}",
{
isUnitPage: ${json.dumps(is_unit_page)}
}
); );
}); });
</%block> </%block>
......
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span class="xblock-display-name">Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-locator="locator-container" data-request-token="page-render-token"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<script type="text/template" id="paging-header-tpl">
<div class="meta-wrap">
<div class="meta">
<%= messageHtml %>
</div>
<nav class="pagination pagination-compact top">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
</ol>
</nav>
</div>
</script>
<script type="text/template" id="paging-footer-tpl">
<nav class="pagination pagination-full bottom">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
<li class="nav-item page">
<div class="pagination-form">
<label class="page-number-label" for="page-number"><%= gettext("Page number") %></label>
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" />
</div>
<span class="current-page"><%= current_page + 1 %></span>
<span class="page-divider">/</span>
<span class="total-pages"><%= total_pages %></span>
</li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
</ol>
</nav>
</script>
<div class="container-paging-header"></div>
<div class="studio-xblock-wrapper" data-locator="locator-group-A">
<section class="wrapper-xblock level-nesting">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<a href="#" data-tooltip="Expand or Collapse" class="action expand-collapse expand">
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span class="xblock-display-name">Group A</span>
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-request-token="page-render-token">
<div class="studio-xblock-wrapper" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-A2">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="header-actions">
<div class="xblock-header-primary">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-A3">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-A4">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-group-B">
<section class="wrapper-xblock level-nesting">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<a href="#" data-tooltip="Expand or Collapse" class="action expand-collapse expand">
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span class="xblock-display-name">Group B</span>
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-request-token="page-render-token">
<div class="studio-xblock-wrapper" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-B2">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-B3">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
<div class="container-paging-footer"></div>
</div>
</article>
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span class="xblock-display-name">Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-locator="locator-container" data-request-token="page-render-token"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<script type="text/template" id="paging-header-tpl">
<div class="meta-wrap">
<div class="meta">
<%= messageHtml %>
</div>
<nav class="pagination pagination-compact top">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
</ol>
</nav>
</div>
</script>
<script type="text/template" id="paging-footer-tpl">
<nav class="pagination pagination-full bottom">
<ol>
<li class="nav-item previous"><a class="nav-link previous-page-link" href="#"><i class="icon-angle-left"></i> <span class="nav-label"><%= gettext("Previous") %></span></a></li>
<li class="nav-item page">
<div class="pagination-form">
<label class="page-number-label" for="page-number"><%= gettext("Page number") %></label>
<input id="page-number-input" class="page-number-input" name="page-number" type="text" size="4" />
</div>
<span class="current-page"><%= current_page + 1 %></span>
<span class="page-divider">/</span>
<span class="total-pages"><%= total_pages %></span>
</li>
<li class="nav-item next"><a class="nav-link next-page-link" href="#"><span class="nav-label"><%= gettext("Next") %></span> <i class="icon-angle-right"></i></a></li>
</ol>
</nav>
</script>
<div class="container-paging-header"></div>
<div class="studio-xblock-wrapper" data-locator="locator-group-A">
<section class="wrapper-xblock level-nesting">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<a href="#" data-tooltip="Expand or Collapse" class="action expand-collapse expand">
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span class="xblock-display-name">Group A</span>
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-request-token="page-render-token">
<div class="studio-xblock-wrapper" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-A2">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="header-actions">
<div class="xblock-header-primary">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-A3">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-group-B">
<section class="wrapper-xblock level-nesting">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<a href="#" data-tooltip="Expand or Collapse" class="action expand-collapse expand">
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">Expand or Collapse</span>
</a>
<span class="xblock-display-name">Group B</span>
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-request-token="page-render-token">
<div class="studio-xblock-wrapper" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-B2">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="studio-xblock-wrapper" data-locator="locator-component-B3">
<section class="wrapper-xblock level-element">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button"></a>
</li>
<li class="action-item action-duplicate">
<a href="#" class="duplicate-button action-button"></a>
</li>
<li class="action-item action-delete">
<a href="#" class="delete-button action-button"></a>
</li>
<li class="action-item action-drag">
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
</li>
</ul>
</div>
</div>
</header>
<article class="xblock-render"></article>
</section>
</div>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
<div class="container-paging-footer"></div>
</div>
</article>
...@@ -22,8 +22,12 @@ from django.utils.translation import ugettext as _ ...@@ -22,8 +22,12 @@ from django.utils.translation import ugettext as _
<%block name="requirejs"> <%block name="requirejs">
require(["js/factories/library"], function(LibraryFactory) { require(["js/factories/library"], function(LibraryFactory) {
LibraryFactory( LibraryFactory(
${component_templates | n}, ${component_templates | n}, ${json.dumps(xblock_info) | n},
${json.dumps(xblock_info) | n} {
isUnitPage: false,
enable_paging: true,
page_size: 10
}
); );
}); });
</%block> </%block>
......
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
""" """
import logging import logging
from .studio_editable import StudioEditableModule
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, List from xblock.fields import Scope, String, List
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xmodule.studio_editable import StudioEditableModule
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -42,29 +42,55 @@ class LibraryRoot(XBlock): ...@@ -42,29 +42,55 @@ class LibraryRoot(XBlock):
def author_view(self, context): def author_view(self, context):
""" """
Renders the Studio preview view, which supports drag and drop. Renders the Studio preview view.
""" """
fragment = Fragment() fragment = Fragment()
self.render_children(context, fragment, can_reorder=False, can_add=True)
return fragment
def render_children(self, context, fragment, can_reorder=False, can_add=False): # pylint: disable=unused-argument
"""
Renders the children of the module with HTML appropriate for Studio. If can_reorder is True,
then the children will be rendered to support drag and drop.
"""
contents = [] contents = []
for child_key in self.children: # pylint: disable=E1101 paging = context.get('paging', None)
context['reorderable_items'].add(child_key)
children_count = len(self.children) # pylint: disable=no-member
item_start, item_end = 0, children_count
# TODO sort children
if paging:
page_number = paging.get('page_number', 0)
raw_page_size = paging.get('page_size', None)
page_size = raw_page_size if raw_page_size is not None else children_count
item_start, item_end = page_size * page_number, page_size * (page_number + 1)
children_to_show = self.children[item_start:item_end] # pylint: disable=no-member
for child_key in children_to_show: # pylint: disable=E1101
child = self.runtime.get_block(child_key) child = self.runtime.get_block(child_key)
rendered_child = self.runtime.render_child(child, StudioEditableModule.get_preview_view_name(child), context) child_view_name = StudioEditableModule.get_preview_view_name(child)
rendered_child = self.runtime.render_child(child, child_view_name, context)
fragment.add_frag_resources(rendered_child) fragment.add_frag_resources(rendered_child)
contents.append({ contents.append({
'id': unicode(child_key), 'id': child.location.to_deprecated_string(),
'content': rendered_child.content, 'content': rendered_child.content
}) })
fragment.add_content(self.runtime.render_template("studio_render_children_view.html", { fragment.add_content(
'items': contents, self.runtime.render_template("studio_render_paged_children_view.html", {
'xblock_context': context, 'items': contents,
'can_add': True, 'xblock_context': context,
'can_reorder': True, 'can_add': can_add,
})) 'can_reorder': False,
return fragment 'first_displayed': item_start,
'total_children': children_count,
'displayed_children': len(children_to_show)
})
)
@property @property
def display_org_with_default(self): def display_org_with_default(self):
......
...@@ -155,7 +155,6 @@ class VideoStudentViewHandlers(object): ...@@ -155,7 +155,6 @@ class VideoStudentViewHandlers(object):
if transcript_name: if transcript_name:
# Get the asset path for course # Get the asset path for course
asset_path = None
course = self.descriptor.runtime.modulestore.get_course(self.course_id) course = self.descriptor.runtime.modulestore.get_course(self.course_id)
if course.static_asset_path: if course.static_asset_path:
asset_path = course.static_asset_path asset_path = course.static_asset_path
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='static_content.html'/>
% for template_name in ["paging-header", "paging-footer"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
% endfor
<div class="xblock-container-paging-parameters" data-start="${first_displayed}" data-displayed="${displayed_children}" data-total="${total_children}"></div>
<div class="container-paging-header"></div>
% for item in items:
${item['content']}
% endfor
% if can_add:
<div class="add-xblock-component new-component-item adding"></div>
% endif
<div class="container-paging-footer"></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