diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 592352e7d9..9dc77ca33d 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -404,3 +404,8 @@ def i_delete_draft(_step): @step(u'I publish the unit$') def publish_unit(_step): world.select_option('visibility-select', 'public') + + +@step(u'I unpublish the unit$') +def unpublish_unit(_step): + world.select_option('visibility-select', 'private') diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index b0fe18e514..0ca8893f74 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -105,6 +105,14 @@ Feature: CMS.Problem Editor And I click on "delete draft" Then the problem display name is "Blank Common Problem" + Scenario: Problems can be made private after being made public + Given I have created a Blank Common Problem + When I publish the unit + And I click on "edit a draft" + And I click on "delete draft" + And I unpublish the unit + Then I can edit the problem + # Disabled 11/13/2013 after failing in master # The screenshot showed that the LaTeX editor had the text "hi", # but Selenium timed out waiting for the text to appear. diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index d3df55af05..819382d8fe 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -258,12 +258,6 @@ def verify_high_level_source_links(step, visible): msg="Expected not to find the latex button but it is present.") world.cancel_component(step) - if visible: - assert_true(world.is_css_present('.upload-button'), - msg="Expected to find the upload button but it is not present.") - else: - assert_true(world.is_css_not_present('.upload-button'), - msg="Expected not to find the upload button but it is present.") def verify_modified_weight(): diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index b904a65afc..c19c4d198c 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -510,7 +510,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): locator = loc_mapper().translate_location( course_items[0].location.course_id, location, True, True ) - resp = self.client.get_fragment(locator.url_reverse('xblock')) + resp = self.client.get_fragment(locator.url_reverse('xblock', 'student_view')) self.assertEqual(resp.status_code, 200) # TODO: uncomment when preview no longer has locations being returned. # _test_no_locations(self, resp) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 7b5c57019d..46671347af 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -39,7 +39,7 @@ from edxmako.shortcuts import render_to_string from models.settings.course_grading import CourseGradingModel from cms.lib.xblock.runtime import handler_url -__all__ = ['orphan_handler', 'xblock_handler'] +__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler'] log = logging.getLogger(__name__) @@ -109,41 +109,7 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid if request.method == 'GET': accept_header = request.META.get('HTTP_ACCEPT', 'application/json') - if 'application/x-fragment+json' in accept_header: - store = get_modulestore(old_location) - component = store.get_item(old_location) - - # Wrap the generated fragment in the xmodule_editor div so that the javascript - # can bind to it correctly - component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime')) - - try: - editor_fragment = component.render('studio_view') - # catch exceptions indiscriminately, since after this point they escape the - # dungeon and surface as uneditable, unsaveable, and undeletable - # component-goblins. - except Exception as exc: # pylint: disable=W0703 - log.debug("Unable to render studio_view for %r", component, exc_info=True) - editor_fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) - - store.save_xmodule(component) - - preview_fragment = get_preview_fragment(request, component) - - hashed_resources = OrderedDict() - for resource in editor_fragment.resources + preview_fragment.resources: - hashed_resources[hash_resource(resource)] = resource - - return JsonResponse({ - 'html': render_to_string('component.html', { - 'preview': preview_fragment.content, - 'editor': editor_fragment.content, - 'label': component.display_name or component.scope_ids.block_type, - }), - 'resources': hashed_resources.items() - }) - - elif 'application/json' in accept_header: + if 'application/json' in accept_header: fields = request.REQUEST.get('fields', '').split(',') if 'graderType' in fields: # right now can't combine output of this w/ output of _get_module_info, but worthy goal @@ -198,6 +164,68 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid ) +# pylint: disable=unused-argument +@require_http_methods(("GET")) +@login_required +@expect_json +def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, version_guid=None, block=None): + """ + The restful handler for requests for rendered xblock views. + + Returns a json object containing two keys: + html: The rendered html of the view + resources: A list of tuples where the first element is the resource hash, and + the second is the resource description + """ + locator = BlockUsageLocator(package_id=package_id, branch=branch, version_guid=version_guid, block_id=block) + if not has_course_access(request.user, locator): + raise PermissionDenied() + old_location = loc_mapper().translate_locator_to_location(locator) + + accept_header = request.META.get('HTTP_ACCEPT', 'application/json') + + if 'application/x-fragment+json' in accept_header: + store = get_modulestore(old_location) + component = store.get_item(old_location) + + # wrap the generated fragment in the xmodule_editor div so that the javascript + # can bind to it correctly + component.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime')) + + if view_name == 'studio_view': + try: + fragment = component.render('studio_view') + # catch exceptions indiscriminately, since after this point they escape the + # dungeon and surface as uneditable, unsaveable, and undeletable + # component-goblins. + except Exception as exc: # pylint: disable=w0703 + log.debug("unable to render studio_view for %r", component, exc_info=True) + fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) + + store.save_xmodule(component) + + elif view_name == 'student_view': + fragment = get_preview_fragment(request, component) + fragment.content = render_to_string('component.html', { + 'preview': fragment.content, + 'label': component.display_name or component.scope_ids.block_type, + }) + else: + raise Http404 + + hashed_resources = OrderedDict() + for resource in fragment.resources: + hashed_resources[hash_resource(resource)] = resource + + return JsonResponse({ + 'html': fragment.content, + 'resources': hashed_resources.items() + }) + + else: + return HttpResponse(status=406) + + def _save_item(request, usage_loc, item_location, data=None, children=None, metadata=None, nullout=None, grader_type=None, publish=None): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 376f8ff82e..c366626f76 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -565,7 +565,11 @@ class TestEditItem(ItemTest): self.assertNotEqual(draft.data, published.data) # Get problem by 'xblock_handler' - resp = self.client.get('/xblock/' + self.problem_locator, HTTP_ACCEPT='application/x-fragment+json') + resp = self.client.get('/xblock/' + self.problem_locator + '/student_view', HTTP_ACCEPT='application/x-fragment+json') + self.assertEqual(resp.status_code, 200) + + # Activate the editing view + resp = self.client.get('/xblock/' + self.problem_locator + '/studio_view', HTTP_ACCEPT='application/x-fragment+json') self.assertEqual(resp.status_code, 200) # Both published and draft content should still be different diff --git a/cms/static/coffee/spec/views/module_edit_spec.coffee b/cms/static/coffee/spec/views/module_edit_spec.coffee index e9e3db2a29..377fdffb89 100644 --- a/cms/static/coffee/spec/views/module_edit_spec.coffee +++ b/cms/static/coffee/spec/views/module_edit_spec.coffee @@ -55,6 +55,7 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod describe "render", -> beforeEach -> spyOn(@moduleEdit, 'loadDisplay') + spyOn(@moduleEdit, 'loadEdit') spyOn(@moduleEdit, 'delegateEvents') spyOn($.fn, 'append') spyOn($, 'getScript') @@ -74,15 +75,58 @@ define ["jquery", "coffee/src/views/module_edit", "js/models/module_info", "xmod ] ) - it "loads the module preview and editor via ajax on the view element", -> + it "loads the module preview via ajax on the view element", -> expect($.ajax).toHaveBeenCalledWith( - url: "/xblock/#{@moduleEdit.model.id}" + url: "/xblock/#{@moduleEdit.model.id}/student_view" + type: "GET" + headers: + Accept: 'application/x-fragment+json' + success: jasmine.any(Function) + ) + + expect($.ajax).not.toHaveBeenCalledWith( + url: "/xblock/#{@moduleEdit.model.id}/studio_view" type: "GET" headers: Accept: 'application/x-fragment+json' success: jasmine.any(Function) ) expect(@moduleEdit.loadDisplay).toHaveBeenCalled() + expect(@moduleEdit.loadEdit).not.toHaveBeenCalled() + expect(@moduleEdit.delegateEvents).toHaveBeenCalled() + + it "loads the editing view via ajax on demand", -> + expect($.ajax).not.toHaveBeenCalledWith( + url: "/xblock/#{@moduleEdit.model.id}/studio_view" + type: "GET" + headers: + Accept: 'application/x-fragment+json' + success: jasmine.any(Function) + ) + expect(@moduleEdit.loadEdit).not.toHaveBeenCalled() + + @moduleEdit.clickEditButton({'preventDefault': jasmine.createSpy('event.preventDefault')}) + + $.ajax.mostRecentCall.args[0].success( + html: '