Commit 138cd459 by Andy Armstrong

Merge pull request #3819 from edx/andya/edit-containers

Allow editing of container xblocks/xmodules
parents 172c6d15 6b3e100c
......@@ -14,6 +14,8 @@ Blades: Remove Video player outline. BLD-975.
Blades: Fix Youtube regular expression in video player editor. BLD-967.
Studio: Support editing of containers. STUD-1312.
Blades: Fix displaying transcripts on touch devices. BLD-1033.
Blades: Tolerance expressed in percentage now computes correctly. BLD-522.
......
......@@ -199,25 +199,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
# change not authored by requestor but by xblocks.
store.update_item(xblock, None)
elif view_name == 'student_view' and xblock_has_own_studio_page(xblock):
context = {
'runtime_type': 'studio',
'container_view': False,
'read_only': is_read_only,
'root_xblock': xblock,
}
# For non-leaf xblocks on the unit page, show the special rendering
# which links to the new container page.
html = render_to_string('container_xblock_component.html', {
'xblock_context': context,
'xblock': xblock,
'locator': usage_key,
})
return JsonResponse({
'html': html,
'resources': [],
})
elif view_name in (unit_views + container_views):
is_container_view = (view_name in container_views)
......@@ -245,8 +226,15 @@ def xblock_view_handler(request, usage_key_string, view_name):
# the component div. Note that the container view recursively adds headers
# into the preview fragment, so we don't want to add another header here.
if not is_container_view:
fragment.content = render_to_string('component.html', {
# For non-leaf xblocks, show the special rendering which links to the new container page.
if xblock_has_own_studio_page(xblock):
template = 'container_xblock_component.html'
else:
template = 'component.html'
fragment.content = render_to_string(template, {
'xblock_context': context,
'xblock': xblock,
'locator': usage_key,
'preview': fragment.content,
'label': xblock.display_name or xblock.scope_ids.block_type,
})
......
......@@ -180,12 +180,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_root': is_root,
'is_reorderable': is_reorderable,
}
# For child xblocks with their own page, render a link to the page
if xblock_has_own_studio_page(xblock) and not is_root:
template = 'studio_container_wrapper.html'
else:
template = 'studio_xblock_wrapper.html'
html = render_to_string(template, template_context)
html = render_to_string('studio_xblock_wrapper.html', template_context)
frag = wrap_fragment(frag, html)
return frag
......
......@@ -137,8 +137,6 @@ class ContainerPageTestCase(StudioPageTestCase):
"""
empty_child_container = ItemFactory.create(parent_location=self.vertical.location,
category='split_test', display_name='Split Test')
ItemFactory.create(parent_location=empty_child_container.location,
category='html', display_name='Split Child')
self.validate_preview_html(empty_child_container, self.reorderable_child_view,
can_reorder=False, can_edit=False, can_add=False)
......@@ -148,9 +146,7 @@ class ContainerPageTestCase(StudioPageTestCase):
"""
empty_child_container = ItemFactory.create(parent_location=self.vertical.location,
category='split_test', display_name='Split Test')
ItemFactory.create(parent_location=empty_child_container.location,
category='html', display_name='Split Child')
modulestore('draft').convert_to_draft(self.vertical.location)
draft_empty_child_container = modulestore('draft').convert_to_draft(empty_child_container.location)
self.validate_preview_html(draft_empty_child_container, self.reorderable_child_view,
can_reorder=True, can_edit=False, can_add=False)
can_reorder=True, can_edit=True, can_add=False)
......@@ -60,7 +60,7 @@ class UnitPageTestCase(StudioPageTestCase):
ItemFactory.create(parent_location=child_container.location,
category='html', display_name='grandchild')
self.validate_preview_html(child_container, 'student_view',
can_reorder=True, can_edit=False, can_add=False)
can_reorder=True, can_edit=True, can_add=False)
def test_draft_child_container_preview_html(self):
"""
......@@ -74,4 +74,4 @@ class UnitPageTestCase(StudioPageTestCase):
modulestore('draft').convert_to_draft(self.vertical.location)
draft_child_container = modulestore('draft').get_item(child_container.location)
self.validate_preview_html(draft_child_container, 'student_view',
can_reorder=True, can_edit=False, can_add=False)
can_reorder=True, can_edit=True, can_add=False)
......@@ -15,7 +15,11 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1",
@render()
loadDisplay: ->
XBlock.initializeBlock(@$el.find('.xblock-student_view'))
# Not all components render an inline student view, e.g. child containers which
# instead render a link to a separate container page.
xblockElement = @$el.find('.xblock-student_view')
if xblockElement.length > 0
XBlock.initializeBlock(xblockElement)
createItem: (parent, payload, callback=->) ->
payload.parent_locator = parent
......
......@@ -11,7 +11,7 @@ define(["backbone", "js/utils/module"], function(Backbone, ModuleUtils) {
"is_container": null,
"data": null,
"metadata" : null,
"children": []
"children": null
}
});
return XBlockInfo;
......
......@@ -11,7 +11,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers
getDragHandle, dragComponentVertically, dragComponentAbove,
verifyRequest, verifyNumReorderCalls, respondToRequest, notificationSpy,
rootLocator = 'testCourse/branch/draft/split_test/splitFFF',
rootLocator = 'locator-container',
containerTestUrl = '/xblock/' + rootLocator,
groupAUrl = "/xblock/locator-group-A",
......
......@@ -7,6 +7,7 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
model, containerPage, requests,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'),
mockUpdatedContainerXBlockHtml = readFixtures('mock/mock-updated-container-xblock.underscore'),
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
beforeEach(function () {
......@@ -14,8 +15,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
appendSetFixtures(mockContainerPage);
model = new XBlockInfo({
id: 'testCourse/branch/draft/block/verticalFFF',
display_name: 'Test Unit',
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical'
});
containerPage = new ContainerPage({
......@@ -51,13 +52,14 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
});
};
describe("Basic display", function() {
describe("Initial display", function() {
it('can render itself', function() {
renderContainerPage(mockContainerXBlockHtml, this);
expect(containerPage.$el.select('.xblock-header')).toBeTruthy();
expect(containerPage.$('.wrapper-xblock')).not.toHaveClass('is-hidden');
expect(containerPage.$('.no-container-content')).toHaveClass('is-hidden');
});
it('shows a loading indicator', function() {
requests = create_sinon.requests(this);
containerPage.render();
......@@ -67,6 +69,66 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
});
});
describe("Editing the container", function() {
var newDisplayName = 'New Display Name';
beforeEach(function () {
edit_helpers.installMockXBlock({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
});
afterEach(function() {
edit_helpers.uninstallMockXBlock();
edit_helpers.cancelModalIfShowing();
});
it('can edit itself', function() {
var editButtons, modalElement,
updatedTitle = 'Updated Test Container';
renderContainerPage(mockContainerXBlockHtml, this);
// Click the root edit button
editButtons = containerPage.$('.nav-actions .edit-button');
editButtons.first().click();
modalElement = edit_helpers.getModalElement();
// Expect a request to be made to show the studio view for the container
expect(lastRequest().url).toBe(
'/xblock/locator-container/studio_view'
);
create_sinon.respondWithJson(requests, {
html: mockContainerXBlockHtml,
resources: []
});
expect(edit_helpers.isShowingModal()).toBeTruthy();
// Expect the correct title to be shown
expect(modalElement.find('.modal-window-title').text()).toBe('Editing: Test Container');
// Press the save button and respond with a success message to the save
edit_helpers.pressModalButton('.action-save');
create_sinon.respondWithJson(requests, { });
expect(edit_helpers.isShowingModal()).toBeFalsy();
// Expect the last request be to refresh the container page
expect(lastRequest().url).toBe(
'/xblock/locator-container/container_preview'
);
create_sinon.respondWithJson(requests, {
html: mockUpdatedContainerXBlockHtml,
resources: []
});
// Expect the title and breadcrumb to be updated
expect(containerPage.$('.page-header-title').text().trim()).toBe(updatedTitle);
expect(containerPage.$('.page-header .subtitle a').last().text().trim()).toBe(updatedTitle);
});
});
describe("Editing an xblock", function() {
var newDisplayName = 'New Display Name';
......@@ -87,10 +149,10 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
it('can show an edit modal for a child xblock', function() {
var editButtons;
renderContainerPage(mockContainerXBlockHtml, this);
editButtons = containerPage.$('.edit-button');
// The container renders six mock xblocks, so there should be an equal number of edit buttons
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons.first().click();
editButtons[0].click();
// Make sure that the correct xblock is requested to be edited
expect(lastRequest().url).toBe(
'/xblock/locator-component-A1/studio_view'
......@@ -125,10 +187,10 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(mockContainerXBlockHtml, this);
editButtons = containerPage.$('.edit-button');
// The container renders six mock xblocks, so there should be an equal number of edit buttons
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
editButtons.first().click();
editButtons[0].click();
create_sinon.respondWithJson(requests, {
html: mockXModuleEditor,
resources: []
......
define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/module_info",
"js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "jasmine-stealth"],
function ($, _, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) {
var requests, unitView, initialize, respondWithHtml, verifyComponents, i;
var requests, unitView, initialize, lastRequest, respondWithHtml, verifyComponents, i,
mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore');
respondWithHtml = function(html, requestIndex) {
create_sinon.respondWithJson(
......@@ -13,6 +14,7 @@ define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/m
initialize = function(test) {
var mockXBlockHtml = readFixtures('mock/mock-unit-page-xblock.underscore'),
mockChildContainerHtml = readFixtures('mock/mock-unit-page-child-container.underscore'),
model;
requests = create_sinon.requests(test);
model = new ModuleModel({
......@@ -25,11 +27,13 @@ define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/m
model: model
});
// Respond with renderings for the two xblocks in the unit
// Respond with renderings for the two xblocks in the unit (the second is itself a child container)
respondWithHtml(mockXBlockHtml, 0);
respondWithHtml(mockXBlockHtml, 1);
respondWithHtml(mockChildContainerHtml, 1);
};
lastRequest = function() { return requests[requests.length - 1]; };
verifyComponents = function (unit, locators) {
var components = unit.$(".component");
expect(components.length).toBe(locators.length);
......@@ -174,5 +178,93 @@ define(["jquery", "underscore", "jasmine", "coffee/src/views/unit", "js/models/m
test_link_disabled_during_ajax_call(draft_states[i]);
}
});
describe("Editing an xblock", function() {
var newDisplayName = 'New Display Name';
beforeEach(function () {
edit_helpers.installMockXBlock({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
});
afterEach(function() {
edit_helpers.uninstallMockXBlock();
edit_helpers.cancelModalIfShowing();
});
it('can show an edit modal for a child xblock', function() {
var editButtons;
initialize(this);
editButtons = unitView.$('.edit-button');
// The container renders two mock xblocks
expect(editButtons.length).toBe(2);
editButtons[1].click();
// Make sure that the correct xblock is requested to be edited
expect(lastRequest().url).toBe(
'/xblock/loc_2/studio_view'
);
create_sinon.respondWithJson(requests, {
html: mockXBlockEditorHtml,
resources: []
});
expect(edit_helpers.isShowingModal()).toBeTruthy();
});
});
describe("Editing an xmodule", function() {
var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'),
newDisplayName = 'New Display Name';
beforeEach(function () {
edit_helpers.installMockXModule({
data: "<p>Some HTML</p>",
metadata: {
display_name: newDisplayName
}
});
});
afterEach(function() {
edit_helpers.uninstallMockXModule();
edit_helpers.cancelModalIfShowing();
});
it('can save changes to settings', function() {
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
initialize(this);
editButtons = unitView.$('.edit-button');
// The container renders two mock xblocks
expect(editButtons.length).toBe(2);
editButtons[1].click();
create_sinon.respondWithJson(requests, {
html: mockXModuleEditor,
resources: []
});
modal = $('.edit-xblock-modal');
// 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
create_sinon.respondWithJson(requests, {
id: 'mock-id'
});
// Respond to the request to refresh
respondWithHtml(mockUpdatedXBlockHtml);
// Verify that the xblock was updated
expect(unitView.$('.mock-updated-content').text()).toBe('Mock Update');
});
});
});
});
......@@ -9,11 +9,17 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers
var installMockXBlock, uninstallMockXBlock, installMockXModule, uninstallMockXModule,
mockComponentTemplates, installEditTemplates, showEditModal, verifyXBlockRequest;
installMockXBlock = function() {
installMockXBlock = function(mockResult) {
window.MockXBlock = function(runtime, element) {
return {
var block = {
runtime: runtime
};
if (mockResult) {
block.save = function() {
return mockResult;
};
}
return block;
};
};
......
......@@ -7,6 +7,7 @@ define(["jquery", "js/spec_helpers/view_helpers"],
getModalElement,
isShowingModal,
hideModalIfShowing,
pressModalButton,
cancelModal,
cancelModalIfShowing;
......@@ -37,12 +38,16 @@ define(["jquery", "js/spec_helpers/view_helpers"],
}
};
cancelModal = function(modal) {
var modalElement, cancelButton;
pressModalButton = function(selector, modal) {
var modalElement, button;
modalElement = getModalElement(modal);
cancelButton = modalElement.find('.action-cancel:visible');
expect(cancelButton.length).toBe(1);
cancelButton.click();
button = modalElement.find(selector + ':visible');
expect(button.length).toBe(1);
button.click();
};
cancelModal = function(modal) {
pressModalButton('.action-cancel', modal);
};
cancelModalIfShowing = function(modal) {
......@@ -52,9 +57,11 @@ define(["jquery", "js/spec_helpers/view_helpers"],
};
return $.extend(view_helpers, {
'getModalElement': getModalElement,
'installModalTemplates': installModalTemplates,
'isShowingModal': isShowingModal,
'hideModalIfShowing': hideModalIfShowing,
'pressModalButton': pressModalButton,
'cancelModal': cancelModal,
'cancelModalIfShowing': cancelModalIfShowing
});
......
define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext", "js/views/feedback_notification"],
function ($, _, XBlockView, ModuleUtils, gettext, NotificationView) {
var reorderableClass = '.reorderable-container',
sortableInitializedClass = '.ui-sortable',
studioXBlockWrapperClass = '.studio-xblock-wrapper';
var ContainerView = XBlockView.extend({
......@@ -8,7 +9,7 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
xblockReady: function () {
XBlockView.prototype.xblockReady.call(this);
var reorderableContainer = this.$(reorderableClass),
alreadySortable = this.$('.ui-sortable'),
alreadySortable = this.$(sortableInitializedClass),
newParent,
oldParent,
self = this;
......@@ -113,7 +114,7 @@ define(["jquery", "underscore", "js/views/xblock", "js/utils/module", "gettext",
},
refresh: function() {
this.$(reorderableClass).sortable('refresh');
this.$(sortableInitializedClass).sortable('refresh');
}
});
......
......@@ -111,13 +111,9 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
},
getTitle: function() {
var displayName = this.xblockElement.find('.xblock-header .header-details').text().trim();
// If not found, try the old unit page style rendering
var displayName = this.xblockInfo.get('display_name');
if (!displayName) {
displayName = this.xblockElement.find('.component-header').text().trim();
if (!displayName) {
displayName = gettext('Component');
}
displayName = gettext('Component');
}
return interpolate(gettext("Editing: %(title)s"), { title: displayName }, true);
},
......@@ -180,13 +176,16 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal",
findXBlockInfo: function(xblockWrapperElement, defaultXBlockInfo) {
var xblockInfo = defaultXBlockInfo,
xblockElement;
xblockElement,
displayName;
if (xblockWrapperElement.length > 0) {
xblockElement = xblockWrapperElement.find('.xblock');
displayName = xblockWrapperElement.find('.xblock-header .header-details').text().trim();
xblockInfo = new XBlockInfo({
id: xblockWrapperElement.data('locator'),
courseKey: xblockWrapperElement.data('course-key'),
category: xblockElement.data('block-type')
category: xblockElement.data('block-type'),
display_name: displayName
});
}
return xblockInfo;
......
......@@ -46,6 +46,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
} else {
noContentElement.removeClass('is-hidden');
}
self.refreshTitle();
loadingElement.addClass('is-hidden');
self.delegateEvents();
}
......@@ -60,6 +61,12 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
return this.xblockView.model.urlRoot;
},
refreshTitle: function() {
var title = this.$('.xblock-header .header-details span').first().text().trim();
this.$('.page-header-title').text(title);
this.$('.page-header .subtitle a').last().text(title);
},
onXBlockRefresh: function(xblockView) {
this.addButtonActions(xblockView.$el);
this.xblockView.refresh();
......@@ -177,10 +184,9 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification",
*/
refreshXBlock: function(xblockElement) {
var parentElement = xblockElement.parent(),
rootLocator = this.xblockView.model.id,
xblockLocator = xblockElement.data('locator');
if (xblockLocator === rootLocator) {
this.render();
rootLocator = this.xblockView.model.id;
if (xblockElement.length === 0 || xblockElement.data('locator') === rootLocator) {
this.render({ });
} else if (parentElement.hasClass('reorderable-container')) {
this.refreshChildXBlock(xblockElement);
} else {
......
......@@ -334,6 +334,7 @@ p, ul, ol, dl {
.navigation-link {
@extend %cont-truncated;
display: inline-block;
vertical-align: bottom; // correct for extra padding in FF
max-width: 250px;
&.navigation-current {
......
......@@ -230,6 +230,7 @@
vertical-align: middle;
.action-button {
@include transition(all $tmg-f3 linear 0s);
display: block;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
......
......@@ -15,11 +15,8 @@
}
// UI: xblock header
.xblock-header {
.xblock-header-primary {
@include box-sizing(border-box);
@include ui-flexbox();
@extend %ui-align-center-flex;
justify-content: space-between;
border-bottom: 1px solid $gray-l4;
border-radius: ($baseline/5) ($baseline/5) 0 0;
min-height: ($baseline*2.5);
......@@ -28,17 +25,73 @@
.header-details {
@extend %cont-truncated;
@extend %ui-justify-left-flex;
@include ui-flexbox();
width: flex-grid(6,12);
display: inline-block;
width: 50%;
vertical-align: middle;
}
.header-actions {
@include ui-flexbox();
@extend %ui-justify-right-flex;
width: flex-grid(6,12);
display: inline-block;
width: 49%;
vertical-align: middle;
text-align: right;
}
}
.xblock-header-secondary {
overflow: hidden;
border-top: 1px solid $gray-l3;
background-color: $gray-l5;
padding: ($baseline/2) $baseline;
.meta-info {
display: inline-block;
vertical-align: middle;
width: 65%;
font-style: italic;
color: $gray;
}
.actions-list {
width: 34%;
display: inline-block;
vertical-align: middle;
text-align: right;
.action-item {
display: inline-block;
.action-button {
@include transition(all $tmg-f3 linear 0s);
display: block;
width: auto;
height: ($baseline*1.5);
border-radius: 3px;
padding: 3px ($baseline/2) 0 ($baseline/2);
color: $gray-l1;
&:hover {
background-color: $blue;
color: $gray-l6;
}
.action-button-text {
display: inline-block;
vertical-align: middle;
padding: 0 1px;
text-transform: uppercase;
}
&.delete-button:hover {
background-color: $gray-l1;
}
}
[class^="icon-"] {
display: inline-block;
vertical-align: middle;
}
}
}
}
}
......
......@@ -12,13 +12,42 @@
.mast {
border-bottom: none;
padding-bottom: 0;
.page-header {
@extend %t-title;
@include font-size(28);
@include line-height(32);
.subtitle .navigation-link {
color: $gray;
&:hover {
color: $blue;
}
}
}
}
.wrapper-mast .mast.has-navigation .nav-actions {
bottom: -($baseline*.75);
.nav-item .button {
.nav-item {
.edit-button {
@include blue-button;
@extend %t-action4;
padding: ($baseline/4) ($baseline/2);
text-align: center;
.action-button-text {
display: inline-block;
vertical-align: middle;
}
[class^="icon-"] {
display: inline-block;
vertical-align: middle;
}
}
}
}
......@@ -140,6 +169,10 @@ body.view-container .content-primary {
}
.xblock-header {
display: block;
}
.xblock-header-primary {
@include ui-flexbox();
margin-bottom: 0;
border-bottom: none;
......@@ -168,6 +201,10 @@ body.view-container .content-primary {
}
.xblock-header {
display: block;
}
.xblock-header-primary {
display: flex;
margin-bottom: 0;
border-bottom: 1px solid $gray-l4;
......@@ -183,7 +220,7 @@ body.view-container .content-primary {
// STATE: xBlock containers styled as rows.
&.xblock-type-container {
.xblock-header {
.xblock-header-primary {
margin-bottom: 0;
border-bottom: 0;
border-radius: ($baseline/5);
......
......@@ -750,13 +750,15 @@ body.unit {
body.unit {
.component-actions {
.component-actions,
.xblock-header-secondary .actions-list {
.action-item {
display: inline-block;
margin: ($baseline/4) 0 ($baseline/4) ($baseline/4);
.action-button {
@include transition(all $tmg-f3 linear 0s);
display: block;
padding: 0 $baseline/2;
width: auto;
......@@ -770,10 +772,10 @@ body.unit {
}
.action-button-text {
padding-left: 1px;
vertical-align: bottom;
display: inline-block;
vertical-align: middle;
padding: 0 1px;
text-transform: uppercase;
line-height: 17px;
}
&.delete-button:hover {
......@@ -783,7 +785,7 @@ body.unit {
[class^="icon-"] {
display: inline-block;
vertical-align: bottom;
vertical-align: middle;
}
}
}
......@@ -1128,7 +1130,7 @@ body.unit .xblock-type-container {
}
}
.xblock-header {
.xblock-header-primary {
border-bottom: 0;
border-radius: ($baseline/5);
......@@ -1178,6 +1180,44 @@ body.unit .component.editing {
}
}
// UI: special case xblock with no preview, ex. experiment blocks
body.unit .component {
.wrapper-component-action-header.nopreview {
position: relative;
border-bottom: 0;
}
.xblock-header-secondary {
overflow: hidden;
border-top: 1px solid $gray-l3;
background-color: $gray-l5;
padding: ($baseline/2);
.meta-info {
display: inline-block;
vertical-align: middle;
width: 65%;
padding-left: ($baseline/4);
font-style: italic;
color: $gray;
}
.actions-list {
display: inline-block;
vertical-align: middle;
width: 33%;
text-align: right;
.action-item {
margin: 0;
}
}
}
}
body.view-unit .main-column .unit-body,
body.view-container {
......
......@@ -54,9 +54,9 @@ main_xblock_info = {
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
<header class="mast has-actions has-navigation">
<header class="mast has-actions has-navigation has-subtitle">
<h1 class="page-header">
<small class="navigation navigation-parents">
<small class="navigation navigation-parents subtitle">
% for ancestor in ancestor_xblocks:
<%
ancestor_url = xblock_studio_url(ancestor)
......@@ -68,11 +68,20 @@ main_xblock_info = {
% endfor
<a href="#" class="navigation-link navigation-current">${xblock.display_name_with_default | h}</a>
</small>
<span class="page-header-title">${xblock.display_name_with_default | h}</span>
</h1>
<nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
% if not unit_publish_state == 'public':
<li class="action-item action-edit nav-item">
<a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
% endif
</ul>
</nav>
</header>
......
......@@ -4,24 +4,41 @@ from contentstore.views.helpers import xblock_studio_url
%>
<%namespace name='static' file='static_content.html'/>
<section class="wrapper-xblock xblock-type-container level-element" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
<header class="xblock-header">
<div class="header-details">
${xblock.display_name_with_default}
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_studio_url(xblock)}" class="action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${_('View')}</span>
<i class="icon-arrow-right"></i>
</a>
</li>
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
</li>
</ul>
</div>
</header>
<section class="wrapper wrapper-xblock wrapper-component-action-header nopreview" data-locator="${locator}" data-course-key="${xblock.location.course_key}">
<div class="component-header">
${xblock.display_name_with_default}
</div>
<ul class="component-actions">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
<li class="action-item action-duplicate">
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
<i class="icon-copy"></i>
<span class="sr">${_("Duplicate")}</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
<i class="icon-trash"></i>
<span class="sr">${_("Delete")}</span>
</a>
</li>
</ul>
</section>
<div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div>
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_studio_url(xblock)}" class="action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${_('View')}</span>
<i class="icon-arrow-right"></i>
</a>
</li>
</ul>
</div>
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
......@@ -3,15 +3,22 @@
<div class="wrapper-mast wrapper" data-location="" data-display-name="" data-category="">
<header class="mast has-actions has-navigation">
<h1 class="page-header">
<small class="navigation navigation-parents">
<small class="navigation navigation-parents subtitle">
<a href="/unit/TestCourse/branch/draft/block/vertical8eb" class="navigation-link navigation-parent">Unit 1</a>
<a href="#" class="navigation-link navigation-current">Nested Vertical Test</a>
<a href="#" class="navigation-link navigation-current">Test Container</a>
</small>
<span class="page-header-title">Test Container</span>
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="action-item action-edit nav-item">
<a href="#" class="button edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
</ul>
</nav>
</header>
......@@ -22,7 +29,7 @@
<section class="content-area">
<article class="content-primary window">
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="TestCourse/branch/draft/block/vertical131">
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container">
</section>
<div class="no-container-content is-hidden">
<p>This page has no content yet.</p>
......
<header class="xblock-header"></header>
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span>Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-block-type="vertical" data-locator="locator-container">
<div class="xblock" data-block-type="vertical" data-locator="locator-container"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="testCourse/branch/draft/split_test/splitFFF">
<div class="xblock" data-block-type="vertical">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A">
<header class="xblock-header">
<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>
</header>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-A">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-A">
<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>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-block-type="vertical">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A1">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A2">
<article class="xblock-render">
<div class="xblock" data-block-type="vertical">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A1">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A1">
<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>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A2">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A2">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A3">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
</ol>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-B">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-B">
<header class="xblock-header">
<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>
</header>
<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>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-A3">
<section class="wrapper-xblock level-element"
data-locator="locator-component-A3">
<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>
</li>
</ol>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-group-B">
<section class="wrapper-xblock level-nesting" data-locator="locator-group-B">
<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>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-block-type="vertical">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B1">
<article class="xblock-render">
<div class="xblock" data-block-type="vertical">
<ol class="reorderable-container">
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B1">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B1">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B2">
<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>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B2">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B2">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B3">
<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>
</li>
<li class="studio-xblock-wrapper is-draggable" data-locator="locator-component-B3">
<section class="wrapper-xblock level-element"
data-locator="locator-component-B3">
<header class="xblock-header">
<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>
</header>
<article class="xblock-render"></article>
</section>
</li>
</ol>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</li>
</ol>
</div>
<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>
</li>
</ol>
<div class="add-xblock-component new-component-item adding"></div>
</div>
</article>
</section>
</li>
</ol>
</div>
......
<section class="wrapper wrapper-xblock wrapper-component-action-header nopreview" data-locator="locator-child-container">
<div class="component-header">
Test Child Container
</div>
<ul class="component-actions">
<li class="action-item action-edit">
<a href="#" class="edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">Edit</span>
</a>
</li>
<li class="action-item action-duplicate">
<a href="#" data-tooltip="Duplicate" class="duplicate-button action-button">
<i class="icon-copy"></i>
<span class="sr">Duplicate</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="Delete" class="delete-button action-button">
<i class="icon-trash"></i>
<span class="sr">Delete</span>
</a>
</li>
</ul>
</section>
<div class="xblock-header-secondary">
<div class="meta-info">This block contains multiple components.</div>
<ul class="actions-list">
<li class="action-item action-view">
<a href="/container/locator-child-container" class="action-button">
<span class="action-button-text">View</span>
<i class="icon-arrow-right"></i>
</a>
</li>
</ul>
</div>
<span data-tooltip="Drag to reorder" class="drag-handle action"></span>
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
<span>Updated Test Container</span>
</div>
<div class="header-actions">
<ul class="actions-list">
</ul>
</div>
</div>
</header>
<article class="xblock-render">
<div class="xblock" data-block-type="vertical" data-locator="locator-container"
data-init="MockXBlock" data-runtime-class="StudioRuntime" data-runtime-version="1">
<ol class="reorderable-container">
</ol>
</div>
</article>
<%!
from django.utils.translation import ugettext as _
from contentstore.views.helpers import xblock_studio_url
%>
<%namespace name='static' file='static_content.html'/>
% if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" data-locator="${xblock.location}">
% else:
<div class="studio-xblock-wrapper">
% endif
<section class="wrapper-xblock xblock-type-container level-element" data-locator="${xblock.location}">
<header class="xblock-header">
<div class="header-details">
${xblock.display_name_with_default}
</div>
<div class="header-actions">
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_studio_url(xblock)}" class="action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${_('View')}</span>
<i class="icon-arrow-right"></i>
</a>
</li>
% if not xblock_context['read_only'] and is_reorderable:
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
</li>
% endif
</ul>
</div>
</header>
</section>
% if is_reorderable:
</li>
% else:
</div>
% endif
<%! from django.utils.translation import ugettext as _ %>
<%!
from django.utils.translation import ugettext as _
from contentstore.views.helpers import xblock_studio_url
%>
<%
xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else ""
%>
% if not is_root:
% if is_reorderable:
......@@ -7,16 +17,13 @@
<div class="studio-xblock-wrapper" data-locator="${xblock.location}">
% endif
<%
section_class = "level-nesting" if xblock.has_children else "level-element"
collapsible_class = "is-collapsible" if xblock.has_children else ""
%>
<section class="wrapper-xblock ${section_class} ${collapsible_class}" data-course-key="${xblock.location.course_key}">
% endif
<header class="xblock-header">
<header class="xblock-header">
<div class="xblock-header-primary">
<div class="header-details">
% if xblock.has_children:
% if show_inline:
<a href="#" data-tooltip="${_('Expand or Collapse')}" class="action expand-collapse collapse">
<i class="icon-caret-down ui-toggle-expansion"></i>
<span class="sr">${_('Expand or Collapse')}</span>
......@@ -26,39 +33,56 @@
</div>
<div class="header-actions">
<ul class="actions-list">
% if not xblock_context['read_only']:
% if not xblock.has_children:
<li class="action-item action-edit">
<a href="#" class="edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
<li class="action-item action-duplicate">
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
% if not xblock_context['read_only'] and not is_root:
% if not show_inline:
<li class="action-item action-edit">
<a href="#" class="edit-button action-button">
<i class="icon-pencil"></i>
<span class="action-button-text">${_("Edit")}</span>
</a>
</li>
<li class="action-item action-duplicate">
<a href="#" data-tooltip="${_("Duplicate")}" class="duplicate-button action-button">
<i class="icon-copy"></i>
<span class="sr">${_("Duplicate")}</span>
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
</a>
</li>
<li class="action-item action-delete">
<a href="#" data-tooltip="${_("Delete")}" class="delete-button action-button">
<i class="icon-trash"></i>
<span class="sr">${_("Delete")}</span>
</a>
</li>
</a>
</li>
% endif
% if not is_root and is_reorderable:
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
</li>
% if is_reorderable:
<li class="action-item action-drag">
<span data-tooltip="${_('Drag to reorder')}" class="drag-handle action"></span>
</li>
% endif
% endif
</ul>
</div>
</header>
</div>
% if xblock_url and not is_root:
<div class="xblock-header-secondary">
<div class="meta-info">${_('This block contains multiple components.')}</div>
<ul class="actions-list">
<li class="action-item action-view">
<a href="${xblock_url}" class="action-button">
## Translators: this is a verb describing the action of viewing more details
<span class="action-button-text">${_('View')}</span>
<i class="icon-arrow-right"></i>
</a>
</li>
</ul>
</div>
% endif
</header>
% if is_root or not xblock_url:
<article class="xblock-render">
${content}
</article>
% endif
% if not is_root:
</section>
......
<section class="sequence-edit">
<section class="filters wip">
<ul>
<li>
<h2>Sort:</h2>
<select>
<option value="">Linear Order</option>
<option value="">Recently Modified</option>
<option value="">Type</option>
<option value="">Alphabetically</option>
</select>
</li>
<li>
<h2>Filter:</h2>
<select>
<option value="">All content</option>
<option value="">Videos</option>
<option value="">Problems</option>
<option value="">Labs</option>
<option value="">Tutorials</option>
<option value="">HTML</option>
</select>
<a href="#" class="more">More</a>
</li>
<li class="search">
<input type="search" name="" id="" value="" placeholder="Search" />
</li>
</ul>
</section>
<div class="content">
<section class="modules">
<ol>
<li>
<ol id="sortable">
% for child in module.get_children():
<li class="${module.scope_ids.block_type}">
<a href="#" class="module-edit"
data-id="${child.location}"
data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.display_name_with_default}</a>
<a href="#" class="draggable">handle</a>
</li>
%endfor
</ol>
</li>
</ol>
</section>
</div>
<%include file="metadata-edit.html" />
</section>
class @SequenceDescriptor extends XModule.Descriptor
constructor: (@element) ->
@$tabs = $(@element).find("#sequence-list")
@$tabs.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('#sequence-list li a', @element).map((idx, el) -> $(el).data('id')).toArray()
class @VerticalDescriptor extends XModule.Descriptor
constructor: (@element) ->
@$items = $(@element).find(".vert-mod")
@$items.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('.vert-mod div', @element).map((idx, el) -> $(el).data('id')).toArray()
class @WrapperDescriptor extends XModule.Descriptor
constructor: (@element) ->
console.log 'WrapperDescriptor'
@$items = $(@element).find(".vert-mod")
@$items.sortable(
update: (event, ui) => @update()
)
save: ->
children: $('.vert-mod div', @element).map((idx, el) -> $(el).data('id')).toArray()
......@@ -8,12 +8,13 @@ from webob import Response
from xmodule.progress import Progress
from xmodule.seq_module import SequenceDescriptor
from xmodule.studio_editable import StudioEditableModule
from xmodule.x_module import XModule, module_attr
from lxml import etree
from xblock.core import XBlock
from xblock.fields import Scope, Integer, ReferenceValueDict
from xblock.fields import Scope, Integer, String, ReferenceValueDict
from xblock.fragment import Fragment
log = logging.getLogger('edx.' + __name__)
......@@ -23,6 +24,13 @@ class SplitTestFields(object):
"""Fields needed for split test module"""
has_children = True
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings,
default="Experiment Block"
)
user_partition_id = Integer(
help="Which user partition is used for this test",
scope=Scope.content
......@@ -45,7 +53,7 @@ class SplitTestFields(object):
@XBlock.needs('user_tags') # pylint: disable=abstract-method
@XBlock.wants('partitions')
class SplitTestModule(SplitTestFields, XModule):
class SplitTestModule(SplitTestFields, XModule, StudioEditableModule):
"""
Show the user the appropriate child. Uses the ExperimentState
API to figure out which child to show.
......@@ -177,21 +185,10 @@ class SplitTestModule(SplitTestFields, XModule):
Renders the Studio preview by rendering each child so that they can all be seen and edited.
"""
fragment = Fragment()
contents = []
for child in self.descriptor.get_children():
rendered_child = self.runtime.get_module(child).render('student_view', context)
fragment.add_frag_resources(rendered_child)
contents.append({
'id': child.location.to_deprecated_string(),
'content': rendered_child.content
})
fragment.add_content(self.system.render_template('vert_module.html', {
'items': contents
}))
# Only render the children when this block is being shown as the container
root_xblock = context.get('root_xblock')
if root_xblock and root_xblock.location == self.location:
self.render_children(context, fragment, can_reorder=False)
return fragment
def student_view(self, context):
......@@ -296,3 +293,11 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor):
makes it use module.get_child_descriptors().
"""
return True
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(SplitTestDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
SplitTestDescriptor.due,
])
return non_editable_fields
......@@ -5,25 +5,34 @@ Mixin to support editing in Studio.
class StudioEditableModule(object):
"""
Helper methods for supporting Studio editing of xblocks.
Helper methods for supporting Studio editing of xblocks/xmodules.
This class is only intended to be used with an XModule, as it assumes the existence of
self.descriptor and self.system.
"""
def render_reorderable_children(self, context, fragment):
def render_children(self, context, fragment, can_reorder=False, can_add=False, view_name='student_view'):
"""
Renders children with the appropriate HTML structure for drag and drop.
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 = []
for child in self.get_display_items():
context['reorderable_items'].add(child.location)
rendered_child = child.render('student_view', context)
for child in self.descriptor.get_children(): # pylint: disable=E1101
if can_reorder:
context['reorderable_items'].add(child.location)
child_module = self.system.get_module(child) # pylint: disable=E1101
rendered_child = child_module.render(view_name, context)
fragment.add_frag_resources(rendered_child)
contents.append({
'id': child.location.to_deprecated_string(),
'content': rendered_child.content
})
fragment.add_content(self.system.render_template("studio_render_children_view.html", {
fragment.add_content(self.system.render_template("studio_render_children_view.html", { # pylint: disable=E1101
'items': contents,
'xblock_context': context,
'can_add': can_add,
'can_reorder': can_reorder,
}))
......@@ -43,7 +43,7 @@ class SplitTestModuleTest(XModuleXmlImportTest):
xml.HtmlFactory(parent=split_test, url_name='split_test_cond1', text='HTML FOR GROUP 1')
self.course = self.process_xml(course)
course_seq = self.course.get_children()[0]
self.course_sequence = self.course.get_children()[0]
self.module_system = get_test_system()
def get_module(descriptor):
......@@ -71,7 +71,7 @@ class SplitTestModuleTest(XModuleXmlImportTest):
)
self.module_system._services['partitions'] = self.partitions_service # pylint: disable=protected-access
self.split_test_module = course_seq.get_children()[0]
self.split_test_module = self.course_sequence.get_children()[0]
self.split_test_module.bind_for_student(self.module_system, self.split_test_module._field_data) # pylint: disable=protected-access
@ddt.data(('0', 'split_test_cond0'), ('1', 'split_test_cond1'))
......@@ -147,3 +147,40 @@ class SplitTestModuleTest(XModuleXmlImportTest):
self.assertEquals(fields.get('user_partition_id'), '0')
self.assertIsNotNone(fields.get('group_id_to_child'))
self.assertEquals(len(children), 2)
def test_render_studio_view(self):
"""
Test the rendering of the Studio view.
"""
# The split_test module should render both its groups when it is the root
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
'root_xblock': self.split_test_module,
}
html = self.module_system.render(self.split_test_module, 'student_view', context).content
self.assertIn('HTML FOR GROUP 0', html)
self.assertIn('HTML FOR GROUP 1', html)
# When rendering as a child, it shouldn't render either of its groups
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
'root_xblock': self.course_sequence,
}
html = self.module_system.render(self.split_test_module, 'student_view', context).content
self.assertNotIn('HTML FOR GROUP 0', html)
self.assertNotIn('HTML FOR GROUP 1', html)
def test_settings(self):
"""
Test the settings configuration.
"""
non_editable_metadata_fields = self.split_test_module.non_editable_metadata_fields
self.assertIn(SplitTestDescriptor.due, non_editable_metadata_fields)
self.assertNotIn(SplitTestDescriptor.display_name, non_editable_metadata_fields)
......@@ -54,9 +54,20 @@ class VerticalModuleTestCase(BaseVerticalModuleTest):
"""
Test the rendering of the Studio view
"""
# Vertical shouldn't render children on the unit page
context = {
'runtime_type': 'studio',
'container_view': False,
}
html = self.module_system.render(self.vertical, 'student_view', context).content
self.assertNotIn(self.test_html_1, html)
self.assertNotIn(self.test_html_2, html)
# Vertical should render reorderable children on the container page
reorderable_items = set()
context = {
'runtime_type': 'studio',
'container_view': True,
'reorderable_items': reorderable_items,
}
html = self.module_system.render(self.vertical, 'student_view', context).content
......
......@@ -30,7 +30,10 @@ class VerticalModule(VerticalFields, XModule, StudioEditableModule):
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
self.render_reorderable_children(context, fragment)
# For the container page we want the full drag-and-drop, but for unit pages we want
# a more concise version that appears alongside the "View =>" link.
if context.get('container_view'):
self.render_children(context, fragment, can_reorder=True, can_add=True)
return fragment
def render_view(self, context, template_name):
......@@ -82,3 +85,11 @@ class VerticalDescriptor(VerticalFields, SequenceDescriptor):
# TODO (victor): Does this need its own definition_to_xml method? Otherwise it looks
# like verticals will get exported as sequentials...
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(VerticalDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([
VerticalDescriptor.due,
])
return non_editable_fields
......@@ -20,7 +20,3 @@ class WrapperDescriptor(VerticalDescriptor):
module_class = WrapperModule
has_children = True
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
......@@ -54,19 +54,19 @@
// extends - justify-content right for display:flex alignment in older browsers
%ui-justify-right-flex {
-webkit-box-pack: end;
-moz-box-pack: end;
-ms-flex-pack: end;
-webkit-justify-content: end;
-webkit-box-pack: flex-end;
-moz-box-pack: flex-end;
-ms-flex-pack: flex-end;
-webkit-justify-content: flex-end;
justify-content: flex-end;
}
// extends - justify-content left for display:flex alignment in older browsers
%ui-justify-left-flex {
-webkit-box-pack: start;
-moz-box-pack: start;
-ms-flex-pack: start;
-webkit-justify-content: start;
-webkit-box-pack: flex-start;
-moz-box-pack: flex-start;
-ms-flex-pack: flex-start;
-webkit-justify-content: flex-start;
justify-content: flex-start;
}
......
from bok_choy.page_object import PageObject
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from utils import click_css
class ComponentEditorView(PageObject):
"""
A :class:`.PageObject` representing the rendered view of a component editor.
This class assumes that the editor is our default editor as displayed for xmodules.
"""
BODY_SELECTOR = '.xblock-editor'
def __init__(self, browser, locator):
"""
Args:
browser (selenium.webdriver): The Selenium-controlled browser that this page is loaded in.
locator (str): The locator that identifies which xblock this :class:`.xblock-editor` relates to.
"""
super(ComponentEditorView, self).__init__(browser)
self.locator = locator
def is_browser_on_page(self):
return self.q(css='{}[data-locator="{}"]'.format(self.BODY_SELECTOR, self.locator)).present
def _bounded_selector(self, selector):
"""
Return `selector`, but limited to this particular `ComponentEditorView` context
"""
return '{}[data-locator="{}"] {}'.format(
self.BODY_SELECTOR,
self.locator,
selector
)
def url(self):
"""
Returns None because this is not directly accessible via URL.
"""
return None
def get_setting_entry_index(self, label):
"""
Returns the index of the setting entry with given label (display name) within the Settings modal.
"""
# TODO: will need to handle tabbed "Settings" in future (current usage is in vertical, only shows Settings.
setting_labels = self.q(css=self._bounded_selector('.metadata_edit .wrapper-comp-setting .setting-label'))
for index, setting in enumerate(setting_labels):
if setting.text == label:
return index
return None
def set_field_value_and_save(self, label, value):
"""
Set the field with given label (display name) to the specified value, and presses Save.
"""
index = self.get_setting_entry_index(label)
elem = self.q(css=self._bounded_selector('.metadata_edit div.wrapper-comp-setting input.setting-input'))[index]
# Click in the field, delete the value there.
action = ActionChains(self.browser).click(elem)
for _x in range(0, len(elem.get_attribute('value'))):
action = action.send_keys(Keys.BACKSPACE)
# Send the new text, then Tab to move to the next field (so change event is triggered).
action.send_keys(value).send_keys(Keys.TAB).perform()
click_css(self, 'a.action-save')
......@@ -3,7 +3,7 @@ Container page in Studio
"""
from bok_choy.page_object import PageObject
from bok_choy.promise import Promise
from bok_choy.promise import Promise, EmptyPromise
from . import BASE_URL
from selenium.webdriver.common.action_chains import ActionChains
......@@ -15,15 +15,24 @@ class ContainerPage(PageObject):
"""
Container page in Studio
"""
NAME_SELECTOR = 'a.navigation-current'
def __init__(self, browser, unit_locator):
def __init__(self, browser, locator):
super(ContainerPage, self).__init__(browser)
self.unit_locator = unit_locator
self.locator = locator
@property
def url(self):
"""URL to the container page for an xblock."""
return "{}/container/{}".format(BASE_URL, self.unit_locator)
return "{}/container/{}".format(BASE_URL, self.locator)
@property
def name(self):
titles = self.q(css=self.NAME_SELECTOR).text
if titles:
return titles[0]
else:
return None
def is_browser_on_page(self):
......@@ -91,6 +100,14 @@ class ContainerPage(PageObject):
# Click the confirmation dialog button
click_css(self, 'a.button.action-primary', 0)
def edit(self):
self.q(css='.edit-button').first.click()
EmptyPromise(
lambda: self.q(css='.xblock-studio_view').present,
'Wait for the Studio editor to be present'
).fulfill()
return self
class XBlockWrapper(PageObject):
......
......@@ -5,7 +5,7 @@ from bok_choy.promise import Promise
from selenium.webdriver.common.action_chains import ActionChains
def click_css(page, css, source_index, require_notification=True):
def click_css(page, css, source_index=0, require_notification=True):
"""
Click the button/link with the given css and index on the specified page (subclass of PageObject).
......
"""
Acceptance tests for Studio related to the container page.
"""
from ..pages.studio.auto_auth import AutoAuthPage
from ..pages.studio.overview import CourseOutlinePage
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
from .helpers import UniqueCourseTest
from ..pages.studio.component_editor import ComponentEditorView
from unittest import skip
class ContainerBase(UniqueCourseTest):
......@@ -85,13 +89,17 @@ class ContainerBase(UniqueCourseTest):
).install()
def go_to_container_page(self, make_draft=False):
unit = self.go_to_unit_page(make_draft)
container = unit.components[0].go_to_container()
return container
def go_to_unit_page(self, make_draft=False):
self.outline.visit()
subsection = self.outline.section('Test Section').subsection('Test Subsection')
unit = subsection.toggle_expand().unit('Test Unit').go_to()
if make_draft:
unit.edit_draft()
container = unit.components[0].go_to_container()
return container
return unit
def verify_ordering(self, container, expected_orderings):
xblocks = container.xblocks
......@@ -131,6 +139,7 @@ class DragAndDropTest(ContainerBase):
expected_ordering
)
@skip("Sporadically drags outside of the Group.")
def test_reorder_in_group(self):
"""
Drag Group A Item 2 before Group A Item 1.
......@@ -303,3 +312,36 @@ class DeleteComponentTest(ContainerBase):
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
{self.group_empty: []}]
self.delete_and_verify(self.group_a_item_1_action_index, expected_ordering)
class EditContainerTest(ContainerBase):
"""
Tests of editing a container.
"""
__test__ = True
def modify_display_name_and_verify(self, component):
"""
Helper method for changing a display name.
"""
modified_name = 'modified'
self.assertNotEqual(component.name, modified_name)
component.edit()
component_editor = ComponentEditorView(self.browser, component.locator)
component_editor.set_field_value_and_save('Display Name', modified_name)
self.assertEqual(component.name, modified_name)
def test_edit_container_on_unit_page(self):
"""
Test the "edit" button on a container appearing on the unit page.
"""
unit = self.go_to_unit_page(make_draft=True)
component = unit.components[0]
self.modify_display_name_and_verify(component)
def test_edit_container_on_container_page(self):
"""
Test the "edit" button on a container appearing on the container page.
"""
container = self.go_to_container_page(make_draft=True)
self.modify_display_name_and_verify(container)
<ol class="reorderable-container">
% if can_reorder:
<ol class="reorderable-container">
% endif
% for item in items:
${item['content']}
% endfor
</ol>
% if not xblock_context['read_only']:
% if can_reorder:
</ol>
% endif
% if can_add and not xblock_context['read_only']:
<div class="add-xblock-component new-component-item adding"></div>
% endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment