diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 12924fba22eb..038b4458b050 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -396,27 +396,3 @@ def create_other_user(_step, name, has_extra_perms, role_name): def log_out(_step): world.visit('logout') - -@step(u'I click on "edit a draft"$') -def i_edit_a_draft(_step): - world.css_click("a.create-draft") - - -@step(u'I click on "replace with draft"$') -def i_replace_w_draft(_step): - world.css_click("a.publish-draft") - - -@step(u'I click on "delete draft"$') -def i_delete_draft(_step): - world.css_click("a.delete-draft") - - -@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/component.py b/cms/djangoapps/contentstore/features/component.py index 05598065b582..ea1cee8d62aa 100644 --- a/cms/djangoapps/contentstore/features/component.py +++ b/cms/djangoapps/contentstore/features/component.py @@ -6,7 +6,7 @@ # pylint: disable=W0613 from lettuce import world, step -from nose.tools import assert_true, assert_in # pylint: disable=E0611 +from nose.tools import assert_true, assert_in, assert_equal # pylint: disable=E0611 DISPLAY_NAME = "Display Name" @@ -48,7 +48,7 @@ def add_a_multi_step_component(step, is_advanced, category): def see_a_multi_step_component(step, category): # Wait for all components to finish rendering - selector = 'li.component div.xblock-student_view' + selector = 'li.studio-xblock-wrapper div.xblock-student_view' world.wait_for(lambda _: len(world.css_find(selector)) == len(step.hashes)) for idx, step_hash in enumerate(step.hashes): @@ -79,7 +79,7 @@ def see_a_problem_component(step, category): assert_true(world.is_css_present(component_css), 'No problem was added to the unit.') - problem_css = 'li.component div.xblock-student_view' + problem_css = 'li.studio-xblock-wrapper div.xblock-student_view' actual_text = world.css_text(problem_css) assert_in(category.upper(), actual_text) @@ -93,7 +93,7 @@ def add_component_category(step, component, category): @step(u'I delete all components$') def delete_all_components(step): - count = len(world.css_find('ol.components li.component')) + count = len(world.css_find('ol.reorderable-container li.studio-xblock-wrapper')) step.given('I delete "' + str(count) + '" component') @@ -124,7 +124,7 @@ def delete_components(step, number): @step(u'I see no components') def see_no_components(steps): - assert world.is_css_not_present('li.component') + assert world.is_css_not_present('li.studio-xblock-wrapper') @step(u'I delete a component') @@ -162,8 +162,9 @@ def find_problem(_driver): @step(u'I see the display name is "([^"]*)"') def check_component_display_name(step, display_name): - label = world.css_text(".component-header") - assert display_name == label + # The display name for the unit uses the same structure, must differentiate by level-element. + label = world.css_html("section.level-element>header>div>div>span.xblock-display-name") + assert_equal(display_name, label) @step(u'I change the display name to "([^"]*)"') diff --git a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py index a4d777d20e5a..e440287c2c71 100644 --- a/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py +++ b/cms/djangoapps/contentstore/features/component_settings_editor_helpers.py @@ -122,9 +122,9 @@ def ensure_settings_visible(): @world.absorb -def edit_component(): +def edit_component(index=0): world.wait_for(lambda _driver: world.css_visible('a.edit-button')) - world.css_click('a.edit-button') + world.css_click('a.edit-button', index) world.wait_for_ajax_complete() diff --git a/cms/djangoapps/contentstore/features/course-export.py b/cms/djangoapps/contentstore/features/course-export.py index 2f428d5bd2d8..22f3fa587868 100644 --- a/cms/djangoapps/contentstore/features/course-export.py +++ b/cms/djangoapps/contentstore/features/course-export.py @@ -55,5 +55,5 @@ def i_click_on_error_dialog(step): # we don't know the actual ID of the vertical. So just check that we did go to a # vertical page in the course (there should only be one). vertical_usage_key = course_key.make_usage_key("vertical", "") - vertical_url = reverse_usage_url('unit_handler', vertical_usage_key) + vertical_url = reverse_usage_url('container_handler', vertical_usage_key) assert_equal(1, world.browser.url.count(vertical_url)) diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index d947493ad161..4b3292665ccd 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.feature +++ b/cms/djangoapps/contentstore/features/problem-editor.feature @@ -81,38 +81,6 @@ Feature: CMS.Problem Editor When I edit and select Settings Then Edit High Level Source is visible - # This is a very specific scenario that was failing with some of the - # DB rearchitecture changes. It had to do with children IDs being stored - # with @draft at the end. To reproduce, must update children while in draft mode. - Scenario: Problems can be deleted after being public - Given I have created a Blank Common Problem - And I have created another Blank Common Problem - When I publish the unit - And I click on "edit a draft" - And I delete "1" component - And I click on "replace with draft" - And I click on "edit a draft" - And I delete "1" component - Then I see no components - - # This is a very specific scenario for a bug where editing a component in draft - # impacted the published version. - Scenario: Changes to draft problem do not impact published version - Given I have created a Blank Common Problem - When I publish the unit - And I click on "edit a draft" - And I change the display name to "draft" - 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 - Scenario: Cheat sheet visible on toggle Given I have created a Blank Common Problem And I can edit the problem diff --git a/cms/djangoapps/contentstore/features/problem-editor.py b/cms/djangoapps/contentstore/features/problem-editor.py index c0d8cd6fc741..75f191c188c0 100644 --- a/cms/djangoapps/contentstore/features/problem-editor.py +++ b/cms/djangoapps/contentstore/features/problem-editor.py @@ -305,15 +305,13 @@ def i_can_edit_problem(_step): @step(u'I edit first blank advanced problem for annotation response$') def i_edit_blank_problem_for_annotation_response(_step): - edit_css = """$('.component-header:contains("Blank Advanced Problem")').parent().find('a.edit-button').click()""" + world.edit_component(1) text = """ Text of annotation """ - world.browser.execute_script(edit_css) - world.wait_for_ajax_complete() type_in_codemirror(0, text) world.save_component() diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index e19e4776dc20..1638abf81536 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -127,7 +127,7 @@ def check_components_on_page(self, component_types, expected_types): # just pick one vertical descriptor = store.get_items(course.id, category='vertical',) - resp = self.client.get_html(get_url('unit_handler', descriptor[0].location)) + resp = self.client.get_html(get_url('container_handler', descriptor[0].location)) self.assertEqual(resp.status_code, 200) _test_no_locations(self, resp) @@ -153,7 +153,7 @@ def test_malformed_edit_unit_request(self): # just pick one vertical usage_key = course_items[0].id.make_usage_key('vertical', None) - resp = self.client.get_html(get_url('unit_handler', usage_key)) + resp = self.client.get_html(get_url('container_handler', usage_key)) self.assertEqual(resp.status_code, 400) _test_no_locations(self, resp, status_code=400) @@ -1185,7 +1185,7 @@ def _check_verticals(self, items): # Assert is here to make sure that the course being tested actually has verticals (units) to check. self.assertGreater(len(items), 0) for descriptor in items: - resp = self.client.get_html(get_url('unit_handler', descriptor.location)) + resp = self.client.get_html(get_url('container_handler', descriptor.location)) self.assertEqual(resp.status_code, 200) _test_no_locations(self, resp) @@ -1585,7 +1585,7 @@ def test_get_html(handler): # go look at the Edit page unit_key = course_key.make_usage_key('vertical', 'test_vertical') - resp = self.client.get_html(get_url('unit_handler', unit_key)) + resp = self.client.get_html(get_url('container_handler', unit_key)) self.assertEqual(resp.status_code, 200) _test_no_locations(self, resp) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 03de2cbf92f4..2e87b39f7337 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -11,7 +11,6 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from edxmako.shortcuts import render_to_response -from util.date_utils import get_default_time_display from xmodule.modulestore.django import modulestore from xblock.core import XBlock @@ -22,7 +21,7 @@ from xblock.runtime import Mixologist from contentstore.utils import get_lms_link_for_item, compute_publish_state, PublishState, get_modulestore -from contentstore.views.helpers import get_parent_xblock +from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name from models.settings.course_grading import CourseGradingModel from opaque_keys.edx.keys import UsageKey @@ -33,14 +32,13 @@ __all__ = ['OPEN_ENDED_COMPONENT_TYPES', 'ADVANCED_COMPONENT_POLICY_KEY', 'subsection_handler', - 'unit_handler', 'container_handler', 'component_handler' ] log = logging.getLogger(__name__) -# NOTE: unit_handler assumes this list is disjoint from ADVANCED_COMPONENT_TYPES +# NOTE: it is assumed that this list is disjoint from ADVANCED_COMPONENT_TYPES COMPONENT_TYPES = ['discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] @@ -154,84 +152,6 @@ def _load_mixed_class(category): return mixologist.mix(component_class) -@require_GET -@login_required -def unit_handler(request, usage_key_string): - """ - The restful handler for unit-specific requests. - - GET - html: return html page for editing a unit - json: not currently supported - """ - if 'text/html' in request.META.get('HTTP_ACCEPT', 'text/html'): - usage_key = UsageKey.from_string(usage_key_string) - try: - course, item, lms_link = _get_item_in_course(request, usage_key) - except ItemNotFoundError: - return HttpResponseBadRequest() - - component_templates = get_component_templates(course) - - xblocks = item.get_children() - - # TODO (cpennington): If we share units between courses, - # this will need to change to check permissions correctly so as - # to pick the correct parent subsection - containing_subsection = get_parent_xblock(item) - containing_section = get_parent_xblock(containing_subsection) - - # cdodge hack. We're having trouble previewing drafts via jump_to redirect - # so let's generate the link url here - - # need to figure out where this item is in the list of children as the - # preview will need this - index = 1 - for child in containing_subsection.get_children(): - if child.location == item.location: - break - index = index + 1 - - preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE') - - preview_lms_link = ( - u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}' - ).format( - preview_lms_base=preview_lms_base, - lms_base=settings.LMS_BASE, - org=course.location.org, - course=course.location.course, - course_name=course.location.name, - section=containing_section.location.name, - subsection=containing_subsection.location.name, - index=index - ) - - return render_to_response('unit.html', { - 'context_course': course, - 'unit': item, - 'unit_usage_key': usage_key, - 'child_usage_keys': [block.scope_ids.usage_id for block in xblocks], - 'component_templates': json.dumps(component_templates), - 'draft_preview_link': preview_lms_link, - 'published_preview_link': lms_link, - 'subsection': containing_subsection, - 'release_date': ( - get_default_time_display(containing_subsection.start) - if containing_subsection.start is not None else None - ), - 'section': containing_section, - 'new_unit_category': 'vertical', - 'unit_state': compute_publish_state(item), - 'published_date': ( - get_default_time_display(item.published_date) - if item.published_date is not None else None - ), - }) - else: - return HttpResponseBadRequest("Only supports html requests") - - # pylint: disable=unused-argument @require_GET @login_required @@ -254,25 +174,37 @@ def container_handler(request, usage_key_string): component_templates = get_component_templates(course) ancestor_xblocks = [] parent = get_parent_xblock(xblock) - while parent and parent.category != 'sequential': + + is_unit_page = is_unit(xblock) + unit = xblock if is_unit_page else None + + while parent and parent.category != 'course': + if unit is None and is_unit(parent): + unit = parent ancestor_xblocks.append(parent) parent = get_parent_xblock(parent) ancestor_xblocks.reverse() - unit = ancestor_xblocks[0] if ancestor_xblocks else None - unit_publish_state = compute_publish_state(unit) if unit else None + subsection = get_parent_xblock(unit) if unit else None + section = get_parent_xblock(subsection) if subsection else None + # TODO: correct with publishing story. + unit_publish_state = 'draft' return render_to_response('container.html', { 'context_course': course, # Needed only for display of menus at top of page. 'xblock': xblock, 'unit_publish_state': unit_publish_state, 'xblock_locator': usage_key, - 'unit': None if not ancestor_xblocks else ancestor_xblocks[0], + 'unit': unit, + 'is_unit_page': is_unit_page, + 'subsection': subsection, + 'section': section, + 'new_unit_category': 'vertical', 'ancestor_xblocks': ancestor_xblocks, 'component_templates': json.dumps(component_templates), }) else: - return HttpResponseBadRequest("Only supports html requests") + return HttpResponseBadRequest("Only supports HTML requests") def get_component_templates(course): @@ -304,16 +236,6 @@ def create_template_dict(name, cat, boilerplate_name=None, is_common=False): 'video': _("Video") } - def get_component_display_name(component, default_display_name=None): - """ - Returns the display name for the specified component. - """ - component_class = _load_mixed_class(component) - if hasattr(component_class, 'display_name') and component_class.display_name.default: - return _(component_class.display_name.default) - else: - return default_display_name - component_templates = [] categories = set() # The component_templates array is in the order of "advanced" (if present), followed @@ -324,7 +246,7 @@ def get_component_display_name(component, default_display_name=None): # add the default template with localized display name # TODO: Once mixins are defined per-application, rather than per-runtime, # this should use a cms mixed-in class. (cpennington) - display_name = get_component_display_name(category, _('Blank')) + display_name = xblock_type_display_name(category, _('Blank')) templates_for_category.append(create_template_dict(display_name, category)) categories.add(category) @@ -347,7 +269,7 @@ def get_component_display_name(component, default_display_name=None): for advanced_problem_type in ADVANCED_PROBLEM_TYPES: component = advanced_problem_type['component'] boilerplate_name = advanced_problem_type['boilerplate_name'] - component_display_name = get_component_display_name(component) + component_display_name = xblock_type_display_name(component) templates_for_category.append(create_template_dict(component_display_name, component, boilerplate_name)) categories.add(component) @@ -369,7 +291,7 @@ def get_component_display_name(component, default_display_name=None): if category in ADVANCED_COMPONENT_TYPES and not category in categories: # boilerplates not supported for advanced components try: - component_display_name = get_component_display_name(category) + component_display_name = xblock_type_display_name(category) advanced_component_templates['templates'].append( create_template_dict( component_display_name, diff --git a/cms/djangoapps/contentstore/views/helpers.py b/cms/djangoapps/contentstore/views/helpers.py index f2550adc4e3a..2ff13c443959 100644 --- a/cms/djangoapps/contentstore/views/helpers.py +++ b/cms/djangoapps/contentstore/views/helpers.py @@ -1,8 +1,13 @@ +from __future__ import absolute_import + import logging +from django.conf import settings from django.http import HttpResponse from django.shortcuts import redirect +from django.utils.translation import ugettext as _ from edxmako.shortcuts import render_to_string, render_to_response +from xblock.core import XBlock from xmodule.modulestore.django import modulestore from contentstore.utils import reverse_course_url, reverse_usage_url @@ -11,7 +16,7 @@ EDITING_TEMPLATES = [ "basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal", "add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu", - "add-xblock-component-menu-problem" + "add-xblock-component-menu-problem", "xblock-string-field-editor", ] # points to the temporary course landing page with log in and sign up @@ -88,8 +93,8 @@ def xblock_has_own_studio_page(xblock): are a few exceptions: 1. Courses 2. Verticals that are either: - - themselves treated as units (in which case they are shown on a unit page) - - a direct child of a unit (in which case they are shown on a container page) + - themselves treated as units + - a direct child of a unit 3. XBlocks with children, except for: - sequentials (aka subsections) - chapters (aka sections) @@ -101,7 +106,7 @@ def xblock_has_own_studio_page(xblock): elif category == 'vertical': parent_xblock = get_parent_xblock(xblock) return is_unit(parent_xblock) if parent_xblock else False - elif category in ('sequential', 'chapter'): + elif category == 'sequential': return False # All other xblocks with children have their own page @@ -115,12 +120,30 @@ def xblock_studio_url(xblock): if not xblock_has_own_studio_page(xblock): return None category = xblock.category - parent_xblock = get_parent_xblock(xblock) - parent_category = parent_xblock.category if parent_xblock else None - if category == 'course': + if category in ('course', 'chapter'): return reverse_course_url('course_handler', xblock.location.course_key) - elif category == 'vertical' and parent_category == 'sequential': - # only show the unit page for verticals directly beneath a subsection - return reverse_usage_url('unit_handler', xblock.location) else: return reverse_usage_url('container_handler', xblock.location) + + +def xblock_type_display_name(xblock, default_display_name=None): + """ + Returns the display name for the specified type of xblock. Note that an instance can be passed in + for context dependent names, e.g. a vertical beneath a sequential is a Unit. + + :param xblock: An xblock instance or the type of xblock. + :param default_display_name: The default value to return if no display name can be found. + :return: + """ + + if hasattr(xblock, 'category'): + if is_unit(xblock): + return _('Unit') + category = xblock.category + else: + category = xblock + component_class = XBlock.load_class(category, select=settings.XBLOCK_SELECT_FUNCTION) + if hasattr(component_class, 'display_name') and component_class.display_name.default: + return _(component_class.display_name.default) + else: + return default_display_name diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index f4e226cffcfe..3cb6b848a64a 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -350,7 +350,7 @@ def export_handler(request, course_key_string): 'raw_err_msg': str(exc), 'failed_module': failed_item, 'unit': unit, - 'edit_unit_url': reverse_usage_url("unit_handler", parent.location) if parent else "", + 'edit_unit_url': reverse_usage_url("container_handler", parent.location) if parent else "", 'course_home_url': reverse_course_url("course_handler", course_key), 'export_url': export_url }) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 46ef23777221..0aa643275771 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -23,7 +23,7 @@ from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError from xmodule.modulestore.inheritance import own_metadata -from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW +from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW from util.json_request import expect_json, JsonResponse from util.string_utils import str_to_bool @@ -33,6 +33,7 @@ from .access import has_course_access from .helpers import _xmodule_recurse, xblock_has_own_studio_page from contentstore.utils import compute_publish_state, PublishState +from contentstore.views.helpers import is_unit from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES from contentstore.views.preview import get_preview_fragment from edxmako.shortcuts import render_to_string @@ -181,7 +182,6 @@ def xblock_view_handler(request, usage_key_string, view_name): xblock = store.get_item(usage_key) is_read_only = _is_xblock_read_only(xblock) container_views = ['container_preview', 'reorderable_container_child_preview'] - unit_views = PREVIEW_VIEWS # wrap the generated fragment in the xmodule_editor div so that the javascript # can bind to it correctly @@ -199,8 +199,8 @@ 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 in (unit_views + container_views): - is_container_view = (view_name in container_views) + elif view_name in (PREVIEW_VIEWS + container_views): + is_pages_view = view_name == STUDENT_VIEW # Only the "Pages" view uses student view in Studio # Determine the items to be shown as reorderable. Note that the view # 'reorderable_container_child_preview' is only rendered for xblocks that @@ -210,27 +210,21 @@ def xblock_view_handler(request, usage_key_string, view_name): if view_name == 'reorderable_container_child_preview': reorderable_items.add(xblock.location) - # Only show the new style HTML for the container view, i.e. for non-verticals - # Note: this special case logic can be removed once the unit page is replaced - # with the new container view. + # Set up the context to be passed to each XBlock's render method. context = { - 'container_view': is_container_view, + 'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks + 'is_unit_page': is_unit(xblock), 'read_only': is_read_only, 'root_xblock': xblock if (view_name == 'container_preview') else None, 'reorderable_items': reorderable_items } fragment = get_preview_fragment(request, xblock, context) - # For old-style pages (such as unit and static pages), wrap the preview with - # 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: - # 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, { + + # Note that the container view recursively adds headers into the preview fragment, + # so only the "Pages" view requires that this extra wrapper be included. + if is_pages_view: + fragment.content = render_to_string('component.html', { 'xblock_context': context, 'xblock': xblock, 'locator': usage_key, @@ -258,10 +252,12 @@ def _is_xblock_read_only(xblock): Returns true if the specified xblock is read-only, meaning that it cannot be edited. """ # We allow direct editing of xblocks in DIRECT_ONLY_CATEGORIES (for example, static pages). - if xblock.category in DIRECT_ONLY_CATEGORIES: - return False - component_publish_state = compute_publish_state(xblock) - return component_publish_state == PublishState.public + # if xblock.category in DIRECT_ONLY_CATEGORIES: + # return False + # component_publish_state = compute_publish_state(xblock) + # return component_publish_state == PublishState.public + # TODO: correct with publishing story. + return False def _save_item(request, usage_key, data=None, children=None, metadata=None, nullout=None, diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 8401935c3d8d..ee366d6520bd 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -175,8 +175,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): """ Wraps the results of rendering an XBlock view in a div which adds a header and Studio action buttons. """ - # Only add the Studio wrapper when on the container page. The unit page will remain as is for now. - if context.get('container_view', None) and view in PREVIEW_VIEWS: + # Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now. + if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS: root_xblock = context.get('root_xblock') is_root = root_xblock and xblock.location == root_xblock.location is_reorderable = _is_xblock_reorderable(xblock, context) diff --git a/cms/djangoapps/contentstore/views/tests/test_container_page.py b/cms/djangoapps/contentstore/views/tests/test_container_page.py index ea4c0fda516c..4481fbcf05b4 100644 --- a/cms/djangoapps/contentstore/views/tests/test_container_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py @@ -3,7 +3,6 @@ """ import re -from contentstore.utils import compute_publish_state, PublishState from contentstore.views.tests.utils import StudioPageTestCase from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory @@ -38,10 +37,13 @@ def test_container_html(self): 'data-locator="{0}" data-course-key="{0.course_key}">'.format(self.child_container.location) ), expected_breadcrumbs=( - r'Unit\s*' - r'Split Test' - ).format(re.escape(unicode(self.vertical.location))) + r'\s*Week 1\s*\s*' + r'\s*Lesson 1\s*\s*' + r'\s*Unit\s*' + ).format( + course=re.escape(unicode(self.course.id)), + unit=re.escape(unicode(self.vertical.location)), + ), ) def test_container_on_container_html(self): @@ -66,15 +68,15 @@ def test_container_html(xblock): 'data-locator="{0}" data-course-key="{0.course_key}">'.format(published_container.location) ), expected_breadcrumbs=( - r'Unit\s*' - r'Split Test\s*' - r'Wrapper' + r'\s*Week 1\s*\s*' + r'\s*Lesson 1\s*\s*' + r'\s*Unit\s*\s*' + r'\s*Split Test\s*' ).format( + course=re.escape(unicode(self.course.id)), unit=re.escape(unicode(self.vertical.location)), split_test=re.escape(unicode(self.child_container.location)) - ) + ), ) # Test the published version of the container @@ -92,44 +94,28 @@ def _test_html_content(self, xblock, expected_section_tag, expected_breadcrumbs) and the breadcrumbs trail is correct. """ html = self.get_page_html(xblock) - publish_state = compute_publish_state(xblock) self.assertIn(expected_section_tag, html) - # Verify the navigation link at the top of the page is correct. self.assertRegexpMatches(html, expected_breadcrumbs) - # Verify the link that allows users to change publish status. - expected_message = None - if publish_state == PublishState.public: - expected_message = 'you need to edit unit Unit as a draft.' - else: - expected_message = 'your changes will be published with unit Unit.' - expected_unit_link = expected_message.format(self.vertical.location) - self.assertIn(expected_unit_link, html) - def test_public_container_preview_html(self): """ Verify that a public xblock's container preview returns the expected HTML. """ - self.validate_preview_html(self.vertical, self.container_view, - can_edit=False, can_reorder=False, can_add=False) - self.validate_preview_html(self.child_container, self.container_view, - can_edit=False, can_reorder=False, can_add=False) - self.validate_preview_html(self.child_vertical, self.reorderable_child_view, - can_edit=False, can_reorder=False, can_add=False) + self.validate_preview_html(self.vertical, self.container_view) + self.validate_preview_html(self.child_container, self.container_view) + self.validate_preview_html(self.child_vertical, self.reorderable_child_view) def test_draft_container_preview_html(self): """ Verify that a draft xblock's container preview returns the expected HTML. """ + # TODO: We should delete some of these test cases when doing publishing story. draft_unit = modulestore('draft').convert_to_draft(self.vertical.location) draft_child_container = modulestore('draft').convert_to_draft(self.child_container.location) draft_child_vertical = modulestore('draft').convert_to_draft(self.child_vertical.location) - self.validate_preview_html(draft_unit, self.container_view, - can_edit=True, can_reorder=True, can_add=True) - self.validate_preview_html(draft_child_container, self.container_view, - can_edit=True, can_reorder=True, can_add=True) - self.validate_preview_html(draft_child_vertical, self.reorderable_child_view, - can_edit=True, can_reorder=True, can_add=True) + self.validate_preview_html(draft_unit, self.container_view) + self.validate_preview_html(draft_child_container, self.container_view) + self.validate_preview_html(draft_child_vertical, self.reorderable_child_view) def test_public_child_container_preview_html(self): """ @@ -137,16 +123,15 @@ def test_public_child_container_preview_html(self): """ empty_child_container = ItemFactory.create(parent_location=self.vertical.location, category='split_test', display_name='Split Test') - self.validate_preview_html(empty_child_container, self.reorderable_child_view, - can_reorder=False, can_edit=False, can_add=False) + self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False) def test_draft_child_container_preview_html(self): """ Verify that a draft container rendered as a child of the container page returns the expected HTML. """ + # TODO: We should delete some of these test cases when doing publishing story. empty_child_container = ItemFactory.create(parent_location=self.vertical.location, category='split_test', display_name='Split Test') 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=True, can_add=False) + self.validate_preview_html(draft_empty_child_container, self.reorderable_child_view, can_add=False) diff --git a/cms/djangoapps/contentstore/views/tests/test_helpers.py b/cms/djangoapps/contentstore/views/tests/test_helpers.py index 82c25c2b6ec5..c6e476d9f4a1 100644 --- a/cms/djangoapps/contentstore/views/tests/test_helpers.py +++ b/cms/djangoapps/contentstore/views/tests/test_helpers.py @@ -3,7 +3,7 @@ """ from contentstore.tests.utils import CourseTestCase -from contentstore.views.helpers import xblock_studio_url +from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name from xmodule.modulestore.tests.factories import ItemFactory @@ -11,6 +11,7 @@ class HelpersTestCase(CourseTestCase): """ Unit tests for helpers.py. """ + def test_xblock_studio_url(self): # Verify course URL @@ -20,18 +21,19 @@ def test_xblock_studio_url(self): # Verify chapter URL chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', display_name="Week 1") - self.assertIsNone(xblock_studio_url(chapter)) + self.assertEqual(xblock_studio_url(chapter), + u'/course/slashes:MITx+999+Robot_Super_Course') # Verify lesson URL sequential = ItemFactory.create(parent_location=chapter.location, category='sequential', display_name="Lesson 1") self.assertIsNone(xblock_studio_url(sequential)) - # Verify vertical URL + # Verify unit URL vertical = ItemFactory.create(parent_location=sequential.location, category='vertical', display_name='Unit') self.assertEqual(xblock_studio_url(vertical), - u'/unit/location:MITx+999+Robot_Super_Course+vertical+Unit') + u'/container/location:MITx+999+Robot_Super_Course+vertical+Unit') # Verify child vertical URL child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical', @@ -43,3 +45,23 @@ def test_xblock_studio_url(self): video = ItemFactory.create(parent_location=child_vertical.location, category="video", display_name="My Video") self.assertIsNone(xblock_studio_url(video)) + + def test_xblock_type_display_name(self): + + chapter = ItemFactory.create(parent_location=self.course.location, category='chapter') + sequential = ItemFactory.create(parent_location=chapter.location, category='sequential') + + # Verify unit type display names + vertical = ItemFactory.create(parent_location=sequential.location, category='vertical') + self.assertEqual(xblock_type_display_name(vertical), u'Unit') + self.assertIsNone(xblock_type_display_name('vertical')) + + # Verify video type display names + video = ItemFactory.create(parent_location=vertical.location, category="video") + self.assertEqual(xblock_type_display_name(video), u'Video') + self.assertEqual(xblock_type_display_name('video'), u'Video') + + # Verify split test type display names + split_test = ItemFactory.create(parent_location=vertical.location, category="split_test") + self.assertEqual(xblock_type_display_name(split_test), u'Content Experiment') + self.assertEqual(xblock_type_display_name('split_test'), u'Content Experiment') diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 69f9753d8a52..3e3e32dae391 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -295,7 +295,7 @@ def test_export_failure_top_level(self): Export failure. """ ItemFactory.create(parent_location=self.course.location, category='aawefawef') - self._verify_export_failure(u'/unit/location:MITx+999+Robot_Super_Course+course+Robot_Super_Course') + self._verify_export_failure(u'/container/location:MITx+999+Robot_Super_Course+course+Robot_Super_Course') def test_export_failure_subsection_level(self): """ @@ -307,7 +307,7 @@ def test_export_failure_subsection_level(self): category='aawefawef' ) - self._verify_export_failure(u'/unit/location:MITx+999+Robot_Super_Course+vertical+foo') + self._verify_export_failure(u'/container/location:MITx+999+Robot_Super_Course+vertical+foo') def _verify_export_failure(self, expectedText): """ Export failure helper method. """ diff --git a/cms/djangoapps/contentstore/views/tests/test_preview.py b/cms/djangoapps/contentstore/views/tests/test_preview.py index 25ddce345854..062d01ac12c7 100644 --- a/cms/djangoapps/contentstore/views/tests/test_preview.py +++ b/cms/djangoapps/contentstore/views/tests/test_preview.py @@ -38,7 +38,11 @@ def test_preview_fragment(self): request.session = {} # Call get_preview_fragment directly. - html = get_preview_fragment(request, html, {}).content + context = { + 'reorderable_items': set(), + 'read_only': True + } + html = get_preview_fragment(request, html, context).content # Verify student view html is returned, and the usage ID is as expected. self.assertRegexpMatches( diff --git a/cms/djangoapps/contentstore/views/tests/test_unit_page.py b/cms/djangoapps/contentstore/views/tests/test_unit_page.py index 695ca090b1d0..34eb8c6f1aec 100644 --- a/cms/djangoapps/contentstore/views/tests/test_unit_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_unit_page.py @@ -20,27 +20,11 @@ def setUp(self): self.video = ItemFactory.create(parent_location=self.vertical.location, category="video", display_name="My Video") - def test_public_unit_page_html(self): - """ - Verify that an xblock returns the expected HTML for a public unit page. - """ - html = self.get_page_html(self.vertical) - self.validate_html_for_add_buttons(html) - - def test_draft_unit_page_html(self): - """ - Verify that an xblock returns the expected HTML for a draft unit page. - """ - draft_unit = modulestore('draft').convert_to_draft(self.vertical.location) - html = self.get_page_html(draft_unit) - self.validate_html_for_add_buttons(html) - def test_public_component_preview_html(self): """ Verify that a public xblock's preview returns the expected HTML. """ - self.validate_preview_html(self.video, STUDENT_VIEW, - can_edit=True, can_reorder=True, can_add=False) + self.validate_preview_html(self.video, STUDENT_VIEW, can_add=False) def test_draft_component_preview_html(self): """ @@ -48,8 +32,7 @@ def test_draft_component_preview_html(self): """ modulestore('draft').convert_to_draft(self.vertical.location) draft_video = modulestore('draft').convert_to_draft(self.video.location) - self.validate_preview_html(draft_video, STUDENT_VIEW, - can_edit=True, can_reorder=True, can_add=False) + self.validate_preview_html(draft_video, STUDENT_VIEW, can_add=False) def test_public_child_container_preview_html(self): """ @@ -60,8 +43,7 @@ def test_public_child_container_preview_html(self): category='split_test', display_name='Split Test') 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=True, can_add=False) + self.validate_preview_html(child_container, STUDENT_VIEW, can_add=False) def test_draft_child_container_preview_html(self): """ @@ -74,5 +56,4 @@ def test_draft_child_container_preview_html(self): category='html', display_name='grandchild') 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=True, can_add=False) + self.validate_preview_html(draft_child_container, STUDENT_VIEW, can_add=False) diff --git a/cms/djangoapps/contentstore/views/tests/utils.py b/cms/djangoapps/contentstore/views/tests/utils.py index 046465e35a93..094a789214be 100644 --- a/cms/djangoapps/contentstore/views/tests/utils.py +++ b/cms/djangoapps/contentstore/views/tests/utils.py @@ -41,19 +41,16 @@ def get_preview_html(self, xblock, view_name): resp_content = json.loads(resp.content) return resp_content['html'] - def validate_preview_html(self, xblock, view_name, can_edit=True, can_reorder=True, can_add=True): + def validate_preview_html(self, xblock, view_name, can_add=True): """ Verify that the specified xblock's preview has the expected HTML elements. """ html = self.get_preview_html(xblock, view_name) - self.validate_html_for_add_buttons(html, can_add=can_add) + self.validate_html_for_add_buttons(html, can_add) - # Verify that there are no drag handles for public blocks + # Verify drag handles always appear. drag_handle_html = '' - if can_reorder: - self.assertIn(drag_handle_html, html) - else: - self.assertNotIn(drag_handle_html, html) + self.assertIn(drag_handle_html, html) # Verify that there are no action buttons for public blocks expected_button_html = [ @@ -62,10 +59,7 @@ def validate_preview_html(self, xblock, view_name, can_edit=True, can_reorder=Tr '' ] for button_html in expected_button_html: - if can_edit: - self.assertIn(button_html, html) - else: - self.assertNotIn(button_html, html) + self.assertIn(button_html, html) def validate_html_for_add_buttons(self, html, can_add=True): """ diff --git a/cms/static/coffee/spec/main.coffee b/cms/static/coffee/spec/main.coffee index e55a28dda388..12157a2347bc 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -224,7 +224,6 @@ define([ "js/spec/views/assets_spec", "js/spec/views/container_spec", - "js/spec/views/unit_spec", "js/spec/views/xblock_spec", "js/spec/views/xblock_editor_spec", diff --git a/cms/static/coffee/src/views/unit.coffee b/cms/static/coffee/src/views/unit.coffee deleted file mode 100644 index a1527e531afe..000000000000 --- a/cms/static/coffee/src/views/unit.coffee +++ /dev/null @@ -1,273 +0,0 @@ -define ["jquery", "jquery.ui", "gettext", "backbone", - "js/views/feedback_notification", "js/views/feedback_prompt", - "coffee/src/views/module_edit", "js/models/module_info", - "js/views/baseview", "js/views/components/add_xblock"], -($, ui, gettext, Backbone, NotificationView, PromptView, ModuleEditView, ModuleModel, BaseView, AddXBlockComponent) -> - class UnitEditView extends BaseView - events: - 'click .delete-draft': 'deleteDraft' - 'click .create-draft': 'createDraft' - 'click .publish-draft': 'publishDraft' - 'change .visibility-select': 'setVisibility' - "click .component-actions .duplicate-button": 'duplicateComponent' - - initialize: => - @visibilityView = new UnitEditView.Visibility( - el: @$('.visibility-select') - model: @model - ) - - @locationView = new UnitEditView.LocationState( - el: @$('.section-item.editing a') - model: @model - ) - - @nameView = new UnitEditView.NameEdit( - el: @$('.unit-name-input') - model: @model - ) - - @addXBlockComponent = new AddXBlockComponent( - collection: @options.templates - el: @$('.add-xblock-component') - createComponent: (template) => - return @createComponent(template, "Creating new component").done( - (editor) -> - listPanel = @$newComponentItem.prev() - listPanel.append(editor.$el) - )) - @addXBlockComponent.render() - - @model.on('change:state', @render) - - @$newComponentItem = @$('.new-component-item') - - @$('.components').sortable( - handle: '.drag-handle' - update: (event, ui) => - analytics.track "Reordered Components", - course: course_location_analytics - id: unit_location_analytics - - payload = children : @components() - saving = new NotificationView.Mini - title: gettext('Saving…') - saving.show() - options = success : => - @model.unset('children') - saving.hide() - @model.save(payload, options) - helper: 'clone' - opacity: '0.5' - placeholder: 'component-placeholder' - forcePlaceholderSize: true - axis: 'y' - items: '> .component' - ) - - @$('.component').each (idx, element) => - model = new ModuleModel - id: $(element).data('locator') - new ModuleEditView - el: element, - onDelete: @deleteComponent, - model: model - - createComponent: (data, analytics_message) => - self = this - operation = $.Deferred() - editor = new ModuleEditView( - onDelete: @deleteComponent - model: new ModuleModel() - ) - - callback = -> - operation.resolveWith(self, [editor]) - analytics.track analytics_message, - course: course_location_analytics - unit_id: unit_location_analytics - type: editor.$el.data('locator') - - editor.createItem( - @$el.data('locator'), - data, - callback - ) - - return operation.promise() - - duplicateComponent: (event) => - self = this - event.preventDefault() - $component = $(event.currentTarget).parents('.component') - source_locator = $component.data('locator') - @runOperationShowingMessage(gettext('Duplicating…'), -> - operation = self.createComponent( - {duplicate_source_locator: source_locator}, - "Duplicating " + source_locator); - operation.done( - (editor) -> - originalOffset = @getScrollOffset($component) - $component.after(editor.$el) - # Scroll the window so that the new component replaces the old one - @setScrollOffset(editor.$el, originalOffset) - )) - - components: => @$('.component').map((idx, el) -> $(el).data('locator')).get() - - wait: (value) => - @$('.unit-body').toggleClass("waiting", value) - - render: => - if @model.hasChanged('state') - @$el.toggleClass("edit-state-#{@model.previous('state')} edit-state-#{@model.get('state')}") - @wait(false) - - saveDraft: => - @model.save() - - deleteComponent: (event) => - self = this - event.preventDefault() - @confirmThenRunOperation(gettext('Delete this component?'), - gettext('Deleting this component is permanent and cannot be undone.'), - gettext('Yes, delete this component'), - -> - self.runOperationShowingMessage(gettext('Deleting…'), - -> - $component = $(event.currentTarget).parents('.component') - return $.ajax({ - type: 'DELETE', - url: self.model.urlRoot + "/" + $component.data('locator') - }).success(=> - analytics.track "Deleted a Component", - course: course_location_analytics - unit_id: unit_location_analytics - id: $component.data('locator') - - $component.remove() - # b/c we don't vigilantly keep children up to date - # get rid of it before it hurts someone - self.model.save({children: self.components()}, - { - success: (model) -> - model.unset('children') - }) - ))) - - deleteDraft: (event) -> - @wait(true) - $.ajax({ - type: 'DELETE', - url: @model.url() + "?" + $.param({recurse: true}) - }).success(=> - - analytics.track "Deleted Draft", - course: course_location_analytics - unit_id: unit_location_analytics - - window.location.reload() - ) - - createDraft: (event) -> - self = this - @disableElementWhileRunning($(event.target), -> - self.wait(true) - $.postJSON(self.model.url(), { - publish: 'create_draft' - }, => - analytics.track "Created Draft", - course: course_location_analytics - unit_id: unit_location_analytics - - self.model.set('state', 'draft') - ) - ) - - publishDraft: (event) -> - self = this - @disableElementWhileRunning($(event.target), -> - self.wait(true) - self.saveDraft() - - $.postJSON(self.model.url(), { - publish: 'make_public' - }, => - analytics.track "Published Draft", - course: course_location_analytics - unit_id: unit_location_analytics - - self.model.set('state', 'public') - ) - ) - - setVisibility: (event) -> - if @$('.visibility-select').val() == 'private' - action = 'make_private' - visibility = "private" - else - action = 'make_public' - visibility = "public" - - @wait(true) - - $.postJSON(@model.url(), { - publish: action - }, => - analytics.track "Set Unit Visibility", - course: course_location_analytics - unit_id: unit_location_analytics - visibility: visibility - - @model.set('state', @$('.visibility-select').val())) - - class UnitEditView.NameEdit extends BaseView - events: - 'change .unit-display-name-input': 'saveName' - - initialize: => - @model.on('change:metadata', @render) - @model.on('change:state', @setEnabled) - @setEnabled() - @saveName - @$spinner = $(''); - - render: => - @$('.unit-display-name-input').val(@model.get('metadata').display_name) - - setEnabled: => - disabled = @model.get('state') == 'public' - if disabled - @$('.unit-display-name-input').attr('disabled', true) - else - @$('.unit-display-name-input').removeAttr('disabled') - - saveName: => - # Treat the metadata dictionary as immutable - metadata = $.extend({}, @model.get('metadata')) - metadata.display_name = @$('.unit-display-name-input').val() - @model.save(metadata: metadata) - # Update name shown in the right-hand side location summary. - $('.unit-location .editing .unit-name').html(metadata.display_name) - analytics.track "Edited Unit Name", - course: course_location_analytics - unit_id: unit_location_analytics - display_name: metadata.display_name - - - class UnitEditView.LocationState extends BaseView - initialize: => - @model.on('change:state', @render) - - render: => - @$el.toggleClass("#{@model.previous('state')}-item #{@model.get('state')}-item") - - class UnitEditView.Visibility extends BaseView - initialize: => - @model.on('change:state', @render) - @render() - - render: => - @$el.val(@model.get('state')) - - return UnitEditView diff --git a/cms/static/js/base.js b/cms/static/js/base.js index cfb221066d6b..7f4a30b46858 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -241,7 +241,7 @@ function createNewUnit(e) { function(data) { // redirect to the edit page - window.location = "/unit/" + data['locator']; + window.location = "/container/" + data['locator']; }); } diff --git a/cms/static/js/spec/views/container_spec.js b/cms/static/js/spec/views/container_spec.js index c61d3bd11445..7f57eb1e91d7 100644 --- a/cms/static/js/spec/views/container_spec.js +++ b/cms/static/js/spec/views/container_spec.js @@ -1,7 +1,7 @@ -define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers", +define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "js/views/container", "js/models/xblock_info", "jquery.simulate", "xmodule", "coffee/src/main", "xblock/cms.runtime.v1"], - function ($, create_sinon, view_helpers, ContainerView, XBlockInfo) { + function ($, create_sinon, edit_helpers, ContainerView, XBlockInfo) { describe("Container View", function () { @@ -34,9 +34,10 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers }; beforeEach(function () { - view_helpers.installViewTemplates(); + edit_helpers.installMockXBlock(); + edit_helpers.installViewTemplates(); appendSetFixtures('
'); - notificationSpy = view_helpers.createNotificationSpy(); + notificationSpy = edit_helpers.createNotificationSpy(); model = new XBlockInfo({ id: rootLocator, display_name: 'Test AB Test', @@ -51,6 +52,7 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers }); afterEach(function () { + edit_helpers.uninstallMockXBlock(); containerView.remove(); }); @@ -186,11 +188,11 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers // Drag the first component in Group B to the first group. dragComponentAbove(groupBComponent1, groupAComponent1); - view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 0, 200); - view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 1, 200); - view_helpers.verifyNotificationHidden(notificationSpy); + edit_helpers.verifyNotificationHidden(notificationSpy); }); it('does not hide saving message if failure', function () { @@ -198,9 +200,9 @@ define([ "jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers // Drag the first component in Group B to the first group. dragComponentAbove(groupBComponent1, groupAComponent1); - view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); respondToRequest(requests, 0, 500); - view_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); + edit_helpers.verifyNotificationShowing(notificationSpy, 'Saving'); // Since the first reorder call failed, the removal will not be called. verifyNumReorderCalls(requests, 1); diff --git a/cms/static/js/spec/views/pages/container_spec.js b/cms/static/js/spec/views/pages/container_spec.js index 35cb2e4579e7..a5a7796769e6 100644 --- a/cms/static/js/spec/views/pages/container_spec.js +++ b/cms/static/js/spec/views/pages/container_spec.js @@ -1,22 +1,34 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", - "js/views/feedback_prompt", "js/views/pages/container", "js/models/xblock_info"], + "js/views/feedback_prompt", "js/views/pages/container", "js/models/xblock_info", "jquery.simulate"], function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, XBlockInfo) { describe("ContainerPage", function() { var lastRequest, renderContainerPage, expectComponents, respondWithHtml, - model, containerPage, requests, + model, containerPage, requests, initialDisplayName, 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 () { + var newDisplayName = 'New Display Name'; + edit_helpers.installEditTemplates(); + edit_helpers.installTemplate('xblock-string-field-editor'); appendSetFixtures(mockContainerPage); + edit_helpers.installMockXBlock({ + data: "

Some HTML

", + metadata: { + display_name: newDisplayName + } + }); + + initialDisplayName = 'Test Container'; + model = new XBlockInfo({ id: 'locator-container', - display_name: 'Test Container', + display_name: initialDisplayName, category: 'vertical' }); containerPage = new ContainerPage({ @@ -26,6 +38,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); }); + afterEach(function() { + edit_helpers.uninstallMockXBlock(); + }); + lastRequest = function() { return requests[requests.length - 1]; }; respondWithHtml = function(html) { @@ -55,9 +71,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin 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'); + expect(containerPage.$('.xblock-header').length).toBe(9); + expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden'); }); it('shows a loading indicator', function() { @@ -70,25 +85,27 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); describe("Editing the container", function() { - var newDisplayName = 'New Display Name'; + var updatedDisplayName = 'Updated Test Container', + inlineEditDisplayName, displayNameElement, displayNameInput; - beforeEach(function () { - edit_helpers.installMockXBlock({ - data: "

Some HTML

", - metadata: { - display_name: newDisplayName - } - }); + beforeEach(function() { + displayNameElement = containerPage.$('.page-header-title'); }); afterEach(function() { - edit_helpers.uninstallMockXBlock(); edit_helpers.cancelModalIfShowing(); }); + inlineEditDisplayName = function(newTitle) { + displayNameElement.click(); + expect(displayNameElement).toHaveClass('is-hidden'); + displayNameInput = containerPage.$('.xblock-string-field-editor .xblock-field-input'); + expect(displayNameInput).not.toHaveClass('is-hidden'); + displayNameInput.val(newTitle); + }; + it('can edit itself', function() { - var editButtons, - updatedTitle = 'Updated Test Container'; + var editButtons; renderContainerPage(mockContainerXBlockHtml, this); // Click the root edit button @@ -118,26 +135,49 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin 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); + // Expect the title to have been updated + expect(displayNameElement.text().trim()).toBe(updatedDisplayName); }); - }); - describe("Editing an xblock", function() { - var newDisplayName = 'New Display Name'; + it('can inline edit the display name', function() { + renderContainerPage(mockContainerXBlockHtml, this); + inlineEditDisplayName(updatedDisplayName); + displayNameInput.change(); + create_sinon.respondWithJson(requests, { }); + expect(displayNameInput).toHaveClass('is-hidden'); + expect(displayNameElement).not.toHaveClass('is-hidden'); + expect(displayNameElement.text().trim()).toBe(updatedDisplayName); + expect(containerPage.model.get('display_name')).toBe(updatedDisplayName); + }); - beforeEach(function () { - edit_helpers.installMockXBlock({ - data: "

Some HTML

", - metadata: { - display_name: newDisplayName - } - }); + it('does not change the title when a display name update fails', function() { + renderContainerPage(mockContainerXBlockHtml, this); + inlineEditDisplayName(updatedDisplayName); + displayNameInput.change(); + create_sinon.respondWithError(requests); + expect(displayNameElement).toHaveClass('is-hidden'); + expect(displayNameInput).not.toHaveClass('is-hidden'); + expect(displayNameInput.val().trim()).toBe(updatedDisplayName); + expect(containerPage.model.get('display_name')).toBe(initialDisplayName); + }); + + it('can cancel an inline edit', function() { + var numRequests; + renderContainerPage(mockContainerXBlockHtml, this); + inlineEditDisplayName(updatedDisplayName); + numRequests = requests.length; + displayNameInput.simulate("keydown", { keyCode: $.simulate.keyCode.ESCAPE }); + displayNameInput.simulate("keyup", { keyCode: $.simulate.keyCode.ESCAPE }); + expect(requests.length).toBe(numRequests); + expect(displayNameInput).toHaveClass('is-hidden'); + expect(displayNameElement).not.toHaveClass('is-hidden'); + expect(displayNameElement.text().trim()).toBe(initialDisplayName); + expect(containerPage.model.get('display_name')).toBe(initialDisplayName); }); + }); + describe("Editing an xblock", function() { afterEach(function() { - edit_helpers.uninstallMockXBlock(); edit_helpers.cancelModalIfShowing(); }); @@ -190,6 +230,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); 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 @@ -426,7 +467,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin }); describe('createNewComponent ', function () { - var clickNewComponent, verifyComponents; + var clickNewComponent; clickNewComponent = function (index) { containerPage.$(".new-component .new-component-type a.single-template")[index].click(); diff --git a/cms/static/js/spec/views/unit_spec.js b/cms/static/js/spec/views/unit_spec.js deleted file mode 100644 index 2464fa2653fd..000000000000 --- a/cms/static/js/spec/views/unit_spec.js +++ /dev/null @@ -1,272 +0,0 @@ -define(["jquery", "underscore.string", "jasmine", "coffee/src/views/unit", "js/models/module_info", - "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "jasmine-stealth"], - function ($, str, jasmine, UnitEditView, ModuleModel, create_sinon, edit_helpers) { - var requests, unitView, initialize, lastRequest, respondWithHtml, verifyComponents, i, - mockXBlockEditorHtml = readFixtures('mock/mock-xblock-editor.underscore'); - - respondWithHtml = function(html, requestIndex) { - create_sinon.respondWithJson( - requests, - { html: html, "resources": [] }, - requestIndex - ); - }; - - 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({ - id: 'unit_locator', - state: 'draft' - }); - unitView = new UnitEditView({ - el: $('.main-wrapper'), - templates: edit_helpers.mockComponentTemplates, - model: model - }); - - // Respond with renderings for the two xblocks in the unit (the second is itself a child container) - respondWithHtml(mockXBlockHtml, 0); - respondWithHtml(mockChildContainerHtml, 1); - }; - - lastRequest = function() { return requests[requests.length - 1]; }; - - verifyComponents = function (unit, locators) { - var components = unit.$(".component"); - expect(components.length).toBe(locators.length); - for (i = 0; i < locators.length; i++) { - expect($(components[i]).data('locator')).toBe(locators[i]); - } - }; - - beforeEach(function() { - edit_helpers.installMockXBlock(); - - // needed to stub out the ajax - window.analytics = jasmine.createSpyObj('analytics', ['track']); - window.course_location_analytics = jasmine.createSpy('course_location_analytics'); - window.unit_location_analytics = jasmine.createSpy('unit_location_analytics'); - }); - - afterEach(function () { - edit_helpers.uninstallMockXBlock(); - }); - - describe("UnitEditView", function() { - beforeEach(function() { - edit_helpers.installEditTemplates(); - appendSetFixtures(readFixtures('mock/mock-unit-page.underscore')); - }); - - describe('duplicateComponent', function() { - var clickDuplicate; - - clickDuplicate = function (index) { - unitView.$(".duplicate-button")[index].click(); - }; - - it('sends the correct JSON to the server', function () { - initialize(this); - clickDuplicate(0); - edit_helpers.verifyXBlockRequest(requests, { - "duplicate_source_locator": "loc_1", - "parent_locator": "unit_locator" - }); - }); - - it('inserts duplicated component immediately after source upon success', function () { - initialize(this); - clickDuplicate(0); - create_sinon.respondWithJson(requests, {"locator": "duplicated_item"}); - verifyComponents(unitView, ['loc_1', 'duplicated_item', 'loc_2']); - }); - - it('inserts duplicated component at end if source at end', function () { - initialize(this); - clickDuplicate(1); - create_sinon.respondWithJson(requests, {"locator": "duplicated_item"}); - verifyComponents(unitView, ['loc_1', 'loc_2', 'duplicated_item']); - }); - - it('shows a notification while duplicating', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); - initialize(this); - clickDuplicate(0); - edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/); - create_sinon.respondWithJson(requests, {"locator": "new_item"}); - edit_helpers.verifyNotificationHidden(notificationSpy); - }); - - it('does not insert duplicated component upon failure', function () { - initialize(this); - clickDuplicate(0); - create_sinon.respondWithError(requests); - verifyComponents(unitView, ['loc_1', 'loc_2']); - }); - }); - - describe('createNewComponent ', function () { - var clickNewComponent; - - clickNewComponent = function () { - unitView.$(".new-component .new-component-type a.single-template").click(); - }; - - it('sends the correct JSON to the server', function () { - initialize(this); - clickNewComponent(); - edit_helpers.verifyXBlockRequest(requests, { - "category": "discussion", - "type": "discussion", - "parent_locator": "unit_locator" - }); - }); - - it('inserts new component at end', function () { - initialize(this); - clickNewComponent(); - create_sinon.respondWithJson(requests, {"locator": "new_item"}); - verifyComponents(unitView, ['loc_1', 'loc_2', 'new_item']); - }); - - it('shows a notification while creating', function () { - var notificationSpy = edit_helpers.createNotificationSpy(); - initialize(this); - clickNewComponent(); - edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/); - create_sinon.respondWithJson(requests, {"locator": "new_item"}); - edit_helpers.verifyNotificationHidden(notificationSpy); - }); - - it('does not insert new component upon failure', function () { - initialize(this); - clickNewComponent(); - create_sinon.respondWithError(requests); - verifyComponents(unitView, ['loc_1', 'loc_2']); - }); - }); - - describe("Disabled edit/publish links during ajax call", function() { - var link, - draft_states = [ - { - state: "draft", - selector: ".publish-draft" - }, - { - state: "public", - selector: ".create-draft" - } - ]; - - function test_link_disabled_during_ajax_call(draft_state) { - it("re-enables the " + draft_state.selector + " link once the ajax call returns", function() { - initialize(this); - link = $(draft_state.selector); - expect(link).not.toHaveClass('is-disabled'); - link.click(); - expect(link).toHaveClass('is-disabled'); - create_sinon.respondWithError(requests); - expect(link).not.toHaveClass('is-disabled'); - }); - } - - for (i = 0; i < draft_states.length; i++) { - 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: "

Some HTML

", - 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(str.startsWith(lastRequest().url, '/xblock/loc_2/studio_view')).toBeTruthy(); - create_sinon.respondWithJson(requests, { - html: mockXBlockEditorHtml, - resources: [] - }); - - // Expect that a modal is shown with the correct title - expect(edit_helpers.isShowingModal()).toBeTruthy(); - expect(edit_helpers.getModalTitle()).toBe('Editing: Test Child Container'); - - }); - }); - - describe("Editing an xmodule", function() { - var mockXModuleEditor = readFixtures('mock/mock-xmodule-editor.underscore'), - newDisplayName = 'New Display Name'; - - beforeEach(function () { - edit_helpers.installMockXModule({ - data: "

Some HTML

", - 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'); - }); - }); - - }); - }); diff --git a/cms/static/js/spec/views/xblock_editor_spec.js b/cms/static/js/spec/views/xblock_editor_spec.js index d9af3e32ece7..8b771e2fb4cf 100644 --- a/cms/static/js/spec/views/xblock_editor_spec.js +++ b/cms/static/js/spec/views/xblock_editor_spec.js @@ -85,7 +85,7 @@ define([ "jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helper }); // Give the mock xblock a save method... editor.xblock.save = window.MockDescriptor.save; - editor.save(); + editor.model.save(editor.getXModuleData()); request = requests[requests.length - 1]; response = JSON.parse(request.requestBody); expect(response.metadata.display_name).toBe(testDisplayName); diff --git a/cms/static/js/spec_helpers/edit_helpers.js b/cms/static/js/spec_helpers/edit_helpers.js index 869949b39f31..d59abb3c11c0 100644 --- a/cms/static/js/spec_helpers/edit_helpers.js +++ b/cms/static/js/spec_helpers/edit_helpers.js @@ -83,8 +83,8 @@ define(["jquery", "underscore", "js/spec_helpers/create_sinon", "js/spec_helpers // Add templates needed by the settings editor modal_helpers.installTemplate('metadata-editor'); - modal_helpers.installTemplate('metadata-number-entry'); - modal_helpers.installTemplate('metadata-string-entry'); + modal_helpers.installTemplate('metadata-number-entry', false, 'metadata-number-entry'); + modal_helpers.installTemplate('metadata-string-entry', false, 'metadata-string-entry'); }; showEditModal = function(requests, xblockElement, model, mockHtml, options) { diff --git a/cms/static/js/spec_helpers/view_helpers.js b/cms/static/js/spec_helpers/view_helpers.js index 1cbbd1e5d4fe..3e6aaaca6da7 100644 --- a/cms/static/js/spec_helpers/view_helpers.js +++ b/cms/static/js/spec_helpers/view_helpers.js @@ -1,14 +1,16 @@ /** * Provides helper methods for invoking Studio modal windows in Jasmine tests. */ -define(["jquery", "js/views/feedback_notification", "js/spec_helpers/create_sinon"], - function($, NotificationView, create_sinon) { +define(["jquery", "js/views/feedback_notification"], + function($, NotificationView) { var installTemplate, installViewTemplates, createNotificationSpy, verifyNotificationShowing, verifyNotificationHidden; - installTemplate = function(templateName, isFirst) { - var template = readFixtures(templateName + '.underscore'), + installTemplate = function(templateName, isFirst, templateId) { + var template = readFixtures(templateName + '.underscore'); + if (!templateId) { templateId = templateName + '-tpl'; + } if (isFirst) { setFixtures($(" -% endfor - - -<%block name="jsextra"> - - - -<%block name="content"> -
-
- -
-
-

-
    - % for usage_key in child_usage_keys: -
  1. - % endfor -
-
-
-
- - <% - index_url = utils.reverse_course_url('course_handler', context_course.id) - subsection_url = utils.reverse_usage_url('subsection_handler', subsection.location) - %> - -
-
- diff --git a/cms/templates/ux/reference/container.html b/cms/templates/ux/reference/container.html index 1e0cad0cb3eb..898759039bcf 100644 --- a/cms/templates/ux/reference/container.html +++ b/cms/templates/ux/reference/container.html @@ -108,7 +108,7 @@

Page Actions

-
+

Video

diff --git a/cms/templates/ux/reference/unit.html b/cms/templates/ux/reference/unit.html deleted file mode 100644 index 45d60ec8452b..000000000000 --- a/cms/templates/ux/reference/unit.html +++ /dev/null @@ -1,887 +0,0 @@ -<%inherit file="../../base.html" /> -<%! -from django.core.urlresolvers import reverse -from django.utils.translation import ugettext as _ -%> -<%namespace name="units" file="../../widgets/units.html" /> -<%block name="title">${_("Individual Unit")} -<%block name="bodyclass">is-signedin course unit view-unit - -<%block name="content"> -
-
-
-

You are editing a draft. -

- View the Live Version -
-
-
-

- - -

-
    -
  1. -
    -
    -
    - - -
    - -
    -
    -
    -
    -
    - - -
    - - -
    -
    -
    - -
    -
    -
    -
    - Save - Cancel -
    -
    -
    - - -
    -
      -
    1. -

      September 21

      -
      -
      -

      Words of encouragement! This is a short note that most students will read.

      -

      Anant Agarwal (6.002x Principal Instructor)

      -
      -

      Primary versus Secondary Updates:

      Unfortunately, the internet throws a lot of text at students, and they - do not read everything that they are given. However, many students do read all that they are - given, and so detailed explainations in this section will benefit the most concientious. - Any essential information should be extremely quickly summarized in the primary section for skimmers.

      -

      Star Forum Poster

      - Students appriciate knowing that the course staff is reading what they post, and one of several ways - that you can do this is by acknowledging the star posters in your announcements. -

      -
      -
    2. -
    -
    -
  2. -
  3. - - -
    -
    -
    - - -
    - -
    -
    -
    - -
    -
    -
    - - -
    -
    -
    - - -
    -
    - - - - - - -
    -
    -
    -
    - - -
    -
    -
    -
    - Save - Cancel -
    -
    -
    - - -
    - - -

    Video

    - -
    -
    - -
    - Skip to a navigable version of this video's transcript. - - - -
      -
    1. -
    -
    - - Go back to start of transcript. - -
    -
      -
    -
    - -
    - - -
  4. -
  5. -
    -
    -
    - Randomize Block -
    -
    - -
    -
    - -
    Shows Element - Example Randomize Block could be here.
    -
    -
  6. -
  7. -
    -
    -
    - - -
    - -
    -
    -
    - - - -
    -
    -
    -
    -
      -
    • -
    • -
    • -
    • -
    • -
    • -
    • -
    - -
    - - -
    -
    - - -
    - - - - - - - - -
    - -
    -
    -
    - Save - Cancel -
    -
    -
    - - -
    -
    - - -

    - Blank Common Problem -

    - -
    (0 points)
    - -
    -
    - -
    - - - - -
    -
    -
    - -
    - - -
  8. -
    -
    Add New Component
    - -
    -
    -
    - -
    - Cancel -
    - - -
  9. -
-
-
- - - -
-
- diff --git a/cms/templates/widgets/units.html b/cms/templates/widgets/units.html index c7a0d2e0e981..04777a21d352 100644 --- a/cms/templates/widgets/units.html +++ b/cms/templates/widgets/units.html @@ -1,5 +1,6 @@ <%! from django.utils.translation import ugettext as _ %> -<%! from contentstore.utils import compute_publish_state, reverse_usage_url %> +<%! from contentstore.utils import compute_publish_state %> +<%! from contentstore.views.helpers import xblock_studio_url %>