diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index f27df9c1b5..8e0e5fc14a 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -246,10 +246,6 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v fragment.content = render_to_string('component.html', { 'preview': fragment.content, 'label': component.display_name or component.scope_ids.block_type, - - # Native XBlocks are responsible for persisting their own data, - # so they are also responsible for providing save/cancel buttons. - 'show_save_cancel': isinstance(component, xmodule.x_module.XModuleDescriptor), }) else: raise Http404 diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index dbb5f6335d..5f4f3a46df 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -765,34 +765,3 @@ class TestComponentHandler(TestCase): self.descriptor.handle = create_response self.assertEquals(component_handler(self.request, self.usage_id, 'dummy_handler').status_code, status_code) - - -@ddt.ddt -class TestNativeXBlock(ItemTest): - """ - Test a "native" XBlock (not an XModule shim). - """ - - @ddt.data(('problem', True), ('acid', False)) - @ddt.unpack - def test_save_cancel_buttons(self, category, include_buttons): - """ - Native XBlocks handle their own persistence, so Studio - should not render Save/Cancel buttons for them. - """ - # Create the XBlock - resp = self.create_xblock(category=category) - self.assertEqual(resp.status_code, 200) - native_loc = json.loads(resp.content)['locator'] - - # Render the XBlock - view_url = '/xblock/{locator}/student_view'.format(locator=native_loc) - resp = self.client.get(view_url, HTTP_ACCEPT='application/json') - self.assertEqual(resp.status_code, 200) - - # Check that the save and cancel buttons are hidden for native XBlocks, - # but shown for XModule shim XBlocks - resp_html = json.loads(resp.content)['html'] - assert_func = self.assertIn if include_buttons else self.assertNotIn - assert_func('save-button', resp_html) - assert_func('cancel-button', resp_html) diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee index 79d454442f..03ee524010 100644 --- a/cms/static/coffee/spec/views/module_edit_spec.coffee +++ b/cms/static/coffee/spec/views/module_edit_spec.coffee @@ -25,7 +25,6 @@ define ["jquery", "js/spec_helpers/edit_helpers", "coffee/src/views/module_edit" -
""" edit_helpers.installEditTemplates(true); spyOn($, 'ajax').andReturn(@moduleData) diff --git a/cms/static/coffee/src/views/module_edit.coffee b/cms/static/coffee/src/views/module_edit.coffee index 5ad27168a6..40724afdfa 100644 --- a/cms/static/coffee/src/views/module_edit.coffee +++ b/cms/static/coffee/src/views/module_edit.coffee @@ -47,7 +47,6 @@ define ["jquery", "underscore", "gettext", "xblock/runtime.v1", clickEditButton: (event) -> event.preventDefault() modal = new EditXBlockModal({ - el: $('.edit-xblock-modal'), view: 'student_view' }); modal.edit(this.$el, self.model, { refresh: _.bind(@render, this) }) diff --git a/cms/static/coffee/src/xblock/cms.runtime.v1.coffee b/cms/static/coffee/src/xblock/cms.runtime.v1.coffee index 609461f7c5..60291c43b8 100644 --- a/cms/static/coffee/src/xblock/cms.runtime.v1.coffee +++ b/cms/static/coffee/src/xblock/cms.runtime.v1.coffee @@ -5,74 +5,76 @@ define [ @PreviewRuntime = {} class PreviewRuntime.v1 extends XBlock.Runtime.v1 - handlerUrl: (element, handlerName, suffix, query, thirdparty) -> - uri = URI("/preview/xblock").segment($(element).data('usage-id')) - .segment('handler') - .segment(handlerName) - if suffix? then uri.segment(suffix) - if query? then uri.search(query) - uri.toString() + handlerUrl: (element, handlerName, suffix, query, thirdparty) -> + uri = URI("/preview/xblock").segment($(element).data('usage-id')) + .segment('handler') + .segment(handlerName) + if suffix? then uri.segment(suffix) + if query? then uri.search(query) + uri.toString() @StudioRuntime = {} class StudioRuntime.v1 extends XBlock.Runtime.v1 - constructor: () -> - super() - @savingNotification = new NotificationView.Mini - title: gettext('Saving…') - @alert = new NotificationView.Error - title: "OpenAssessment Save Error", - closeIcon: false, - shown: false + constructor: () -> + super() + @savingNotification = new NotificationView.Mini + title: gettext('Saving…') + @alert = new NotificationView.Error + title: "OpenAssessment Save Error", + closeIcon: false, + shown: false - handlerUrl: (element, handlerName, suffix, query, thirdparty) -> - uri = URI("/xblock").segment($(element).data('usage-id')) - .segment('handler') - .segment(handlerName) - if suffix? then uri.segment(suffix) - if query? then uri.search(query) - uri.toString() + handlerUrl: (element, handlerName, suffix, query, thirdparty) -> + uri = URI("/xblock").segment($(element).data('usage-id')) + .segment('handler') + .segment(handlerName) + if suffix? then uri.segment(suffix) + if query? then uri.search(query) + uri.toString() - # Notify the Studio client-side runtime so it can update - # the UI in a consistent way. Currently, this is used - # for save / cancel when editing an XBlock. - # Although native XBlocks should handle their own persistence, - # Studio still needs to update the UI in a consistent way - # (showing the "Saving..." notification, closing the modal editing dialog, etc.) - notify: (name, data) -> - if name == 'save' - if 'state' of data + # Notify the Studio client-side runtime so it can update + # the UI in a consistent way. Currently, this is used + # for save / cancel when editing an XBlock. + # Although native XBlocks should handle their own persistence, + # Studio still needs to update the UI in a consistent way + # (showing the "Saving..." notification, closing the modal editing dialog, etc.) + notify: (name, data) -> + if name == 'save' + if 'state' of data - # Starting to save, so show the "Saving..." notification - if data.state == 'start' - @savingNotification.show() + # Starting to save, so show the "Saving..." notification + if data.state == 'start' + @savingNotification.show() - # Finished saving, so hide the "Saving..." notification - else if data.state == 'end' + # Finished saving, so hide the "Saving..." notification + else if data.state == 'end' + @_hideAlerts() - # Hide the editor *after* we finish saving in case there are validation - # errors that the user needs to correct. - @_hideEditor() + # Notify the modal that the save has completed so that it can hide itself + # and then refresh the xblock. + if @modal + @modal.onSave() - $('.component.editing').removeClass('editing') - @savingNotification.hide() + @savingNotification.hide() - else if name == 'cancel' - @_hideEditor() + else if name == 'edit-modal-shown' + @modal = data - else if name == 'error' - if 'msg' of data - @alert.options.message = data.msg - @alert.show() + else if name == 'edit-modal-hidden' + @modal = null - _hideEditor: () -> - # This will close all open component editors, which works - # if we assume that <= 1 are open at a time. - el = $('.component.editing') - el.removeClass('editing') - el.find('.component-editor').slideUp(150) - ModalUtils.hideModalCover() + else if name == 'cancel' + @_hideAlerts() + if @modal + @modal.cancel() - # Hide any alerts that are being shown - if @alert.options.shown - @alert.hide() + else if name == 'error' + if 'msg' of data + @alert.options.message = data.msg + @alert.show() + + _hideAlerts: () -> + # Hide any alerts that are being shown + if @alert.options.shown + @alert.hide() diff --git a/cms/static/js/spec/views/modals/edit_xblock_spec.js b/cms/static/js/spec/views/modals/edit_xblock_spec.js index 0605b269a4..e58759dea3 100644 --- a/cms/static/js/spec/views/modals/edit_xblock_spec.js +++ b/cms/static/js/spec/views/modals/edit_xblock_spec.js @@ -5,9 +5,9 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers describe("EditXBlockModal", function() { var model, modal, showModal; - showModal = function(requests, mockHtml) { + showModal = function(requests, mockHtml, options) { var xblockElement = $('.xblock'); - return edit_helpers.showEditModal(requests, xblockElement, model, mockHtml); + return edit_helpers.showEditModal(requests, xblockElement, model, mockHtml, options); }; beforeEach(function () { @@ -45,6 +45,13 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); }); + it('does not show the "Save" button', function() { + var requests = create_sinon.requests(this); + modal = showModal(requests, mockXBlockEditorHtml); + expect(modal.$('.action-save')).not.toBeVisible(); + expect(modal.$('.action-cancel').text()).toBe('OK'); + }); + it('shows the correct title', function() { var requests = create_sinon.requests(this); modal = showModal(requests, mockXBlockEditorHtml); @@ -56,6 +63,43 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers modal = showModal(requests, mockXBlockEditorHtml); expect(modal.$('.editor-modes a').length).toBe(0); }); + + it('hides itself and refreshes after save notification', function() { + var requests = create_sinon.requests(this), + refreshed = false, + refresh = function() { + refreshed = true; + }; + modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); + modal.runtime.notify('save', { state: 'start' }); + modal.runtime.notify('save', { state: 'end' }); + expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(refreshed).toBeTruthy(); + }); + + it('hides itself and does not refresh after cancel notification', function() { + var requests = create_sinon.requests(this), + refreshed = false, + refresh = function() { + refreshed = true; + }; + modal = showModal(requests, mockXBlockEditorHtml, { refresh: refresh }); + modal.runtime.notify('cancel'); + expect(edit_helpers.isShowingModal(modal)).toBeFalsy(); + expect(refreshed).toBeFalsy(); + }); + + describe("Custom Buttons", function() { + var mockCustomButtonsHtml; + + mockCustomButtonsHtml = readFixtures('mock/mock-xblock-editor-with-custom-buttons.underscore'); + + it('hides the modal\'s button bar', function() { + var requests = create_sinon.requests(this); + modal = showModal(requests, mockCustomButtonsHtml); + expect(modal.$('.modal-actions')).toBeHidden(); + }); + }); }); describe("XModule Editor", function() { @@ -64,12 +108,11 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-editor.underscore'); beforeEach(function() { - // Mock the VerticalDescriptor so that the module can be rendered - window.VerticalDescriptor = XModule.Descriptor; + edit_helpers.installMockXModule(); }); afterEach(function () { - window.VerticalDescriptor = null; + edit_helpers.uninstallMockXModule(); }); it('can render itself', function() { @@ -140,12 +183,11 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers mockXModuleEditorHtml = readFixtures('mock/mock-xmodule-settings-only-editor.underscore'); beforeEach(function() { - // Mock the VerticalDescriptor so that the module can be rendered - window.VerticalDescriptor = XModule.Descriptor; + edit_helpers.installMockXModule(); }); afterEach(function () { - window.VerticalDescriptor = null; + edit_helpers.uninstallMockXModule(); }); it('can render itself', function() { diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index a135ca4509..2c5686fd9e 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -107,6 +107,29 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" }); expect(edit_helpers.isShowingModal()).toBeTruthy(); }); + }); + + describe("Editing an xmodule", function() { + var mockContainerXBlockHtml, + mockXModuleEditor, + newDisplayName = 'New Display Name'; + + beforeEach(function () { + edit_helpers.installMockXModule({ + data: "Some HTML
", + metadata: { + display_name: newDisplayName + } + }); + }); + + afterEach(function() { + edit_helpers.uninstallMockXModule(); + edit_helpers.cancelModalIfShowing(); + }); + + mockContainerXBlockHtml = readFixtures('mock/mock-container-xblock.underscore'); + mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'); it('can save changes to settings', function() { var editButtons, modal, mockUpdatedXBlockHtml; @@ -117,7 +140,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers" expect(editButtons.length).toBe(6); editButtons.first().click(); create_sinon.respondWithJson(requests, { - html: mockXBlockEditorHtml, + html: mockXModuleEditor, resources: [] }); diff --git a/cms/static/js/spec/views/xblock_editor_spec.js b/cms/static/js/spec/views/xblock_editor_spec.js index e53fa0f167..73abb41f34 100644 --- a/cms/static/js/spec/views/xblock_editor_spec.js +++ b/cms/static/js/spec/views/xblock_editor_spec.js @@ -49,20 +49,6 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper expect(editor.$el.select('.xblock-header')).toBeTruthy(); expect(editor.getMode()).toEqual('settings'); }); - - it('saves any custom metadata', function() { - var requests = create_sinon.requests(this), request, response; - editor.render(); - create_sinon.respondWithJson(requests, { - html: mockXBlockEditorHtml, - resources: [] - }); - editor.save(); - request = requests[requests.length - 1]; - response = JSON.parse(request.requestBody); - expect(response.metadata.display_name).toBe(testDisplayName); - expect(response.metadata.custom_field).toBe('Custom Value'); - }); }); describe("Editing an xmodule", function() { diff --git a/cms/static/js/spec_helpers/edit_helpers.js b/cms/static/js/spec_helpers/edit_helpers.js index c33b5aac87..5e0a55be4f 100644 --- a/cms/static/js/spec_helpers/edit_helpers.js +++ b/cms/static/js/spec_helpers/edit_helpers.js @@ -1,9 +1,9 @@ /** * Provides helper methods for invoking Studio editors in Jasmine tests. */ -define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", "js/views/modals/edit_xblock", - "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], - function($, create_sinon, modal_helpers, EditXBlockModal) { +define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers", + "js/views/modals/edit_xblock", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], + function($, _, create_sinon, modal_helpers, EditXBlockModal) { var editorTemplate = readFixtures('metadata-editor.underscore'), numberEntryTemplate = readFixtures('metadata-number-entry.underscore'), @@ -12,19 +12,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers editorModeButtonTemplate = readFixtures('editor-mode-button.underscore'), installMockXBlock, uninstallMockXBlock, - hasSavedMockXBlock, installMockXModule, uninstallMockXModule, - hasSavedMockXModule, installEditTemplates, showEditModal; installMockXBlock = function(mockResult) { window.MockXBlock = function(runtime, element) { return { - save: function() { - return mockResult; - } + runtime: runtime }; }; }; @@ -58,9 +54,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/modal_helpers appendSetFixtures($("