From 541d20ef83d33e6872405cbded3794ec97cd52a8 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Fri, 2 May 2014 14:53:31 -0400 Subject: [PATCH] Allow creation of components on container page This commit implements STUD-1490, allowing creation of components on the container page. It also enables the delete and duplicate buttons now that new content can be created that would benefit. Note that it also creates shared functionality for adding components, and refactors the unit page to use it too. --- CHANGELOG.rst | 5 +- .../contentstore/tests/test_contentstore.py | 10 +- .../contentstore/views/component.py | 163 +++--- cms/djangoapps/contentstore/views/helpers.py | 46 +- cms/djangoapps/contentstore/views/item.py | 45 +- cms/djangoapps/contentstore/views/preview.py | 21 +- .../views/tests/test_container.py | 154 ----- .../views/tests/test_container_page.py | 158 +++++ .../contentstore/views/tests/test_tabs.py | 2 +- .../views/tests/test_unit_page.py | 77 +++ .../contentstore/views/tests/utils.py | 81 +++ cms/envs/common.py | 6 - .../coffee/spec/views/module_edit_spec.coffee | 2 +- cms/static/coffee/src/views/unit.coffee | 496 ++++++++-------- .../js/collections/component_template.js | 5 + cms/static/js/models/component_template.js | 31 + cms/static/js/spec/views/baseview_spec.js | 36 +- cms/static/js/spec/views/container_spec.js | 54 +- .../js/spec/views/pages/container_spec.js | 244 +++++--- cms/static/js/spec/views/unit_spec.js | 380 +++++------- .../js/spec/views/xblock_editor_spec.js | 2 +- cms/static/js/spec_helpers/create_sinon.js | 10 +- cms/static/js/spec_helpers/edit_helpers.js | 77 ++- cms/static/js/spec_helpers/modal_helpers.js | 9 +- cms/static/js/spec_helpers/view_helpers.js | 47 +- cms/static/js/utils/module.js | 4 +- cms/static/js/utils/templates.js | 20 + cms/static/js/views/baseview.js | 91 ++- cms/static/js/views/components/add_xblock.js | 74 +++ .../js/views/components/add_xblock_button.js | 13 + .../js/views/components/add_xblock_menu.js | 19 + cms/static/js/views/container.js | 24 +- cms/static/js/views/feedback.js | 219 +++---- cms/static/js/views/pages/container.js | 232 +++++--- cms/static/js/views/xblock.js | 17 +- .../sass/elements/_system-feedback.scss | 12 +- cms/static/sass/elements/_xblocks.scss | 9 +- cms/static/sass/views/_container.scss | 63 +- cms/static/sass/views/_unit.scss | 544 +++++++++--------- cms/templates/component.html | 4 +- cms/templates/container.html | 11 +- cms/templates/container_xblock_component.html | 6 +- cms/templates/edit-tabs.html | 2 +- .../js/add-xblock-component-button.underscore | 8 + ...d-xblock-component-menu-problem.underscore | 47 ++ .../js/add-xblock-component-menu.underscore | 23 + .../js/add-xblock-component.underscore | 5 + .../js/mock/mock-container-page.underscore | 7 +- .../js/mock/mock-container-xblock.underscore | 412 ++++++------- .../mock-empty-container-xblock.underscore | 6 +- .../js/mock/mock-unit-page-xblock.underscore | 27 + .../js/mock/mock-unit-page.underscore | 47 ++ .../js/mock/mock-updated-xblock.underscore | 36 +- cms/templates/js/mock/mock-xblock.underscore | 38 +- cms/templates/overview.html | 6 +- cms/templates/studio_container_wrapper.html | 39 ++ cms/templates/studio_vertical_wrapper.html | 24 - cms/templates/studio_xblock_wrapper.html | 44 +- cms/templates/unit.html | 93 +-- cms/templates/ux/reference/unit.html | 6 +- cms/templates/widgets/units.html | 5 +- common/lib/xmodule/xmodule/studio_editable.py | 30 + .../xmodule/tests/test_studio_editable.py | 25 + .../xmodule/xmodule/tests/test_vertical.py | 65 +++ common/lib/xmodule/xmodule/vertical_module.py | 7 +- .../test/acceptance/pages/studio/container.py | 47 +- common/test/acceptance/pages/studio/utils.py | 35 ++ .../acceptance/tests/test_studio_container.py | 206 +++++-- .../studio_render_children_view.html | 8 + lms/templates/vert_module_studio_view.html | 17 - 70 files changed, 2907 insertions(+), 1931 deletions(-) delete mode 100644 cms/djangoapps/contentstore/views/tests/test_container.py create mode 100644 cms/djangoapps/contentstore/views/tests/test_container_page.py create mode 100644 cms/djangoapps/contentstore/views/tests/test_unit_page.py create mode 100644 cms/djangoapps/contentstore/views/tests/utils.py create mode 100644 cms/static/js/collections/component_template.js create mode 100644 cms/static/js/models/component_template.js create mode 100644 cms/static/js/utils/templates.js create mode 100644 cms/static/js/views/components/add_xblock.js create mode 100644 cms/static/js/views/components/add_xblock_button.js create mode 100644 cms/static/js/views/components/add_xblock_menu.js create mode 100644 cms/templates/js/add-xblock-component-button.underscore create mode 100644 cms/templates/js/add-xblock-component-menu-problem.underscore create mode 100644 cms/templates/js/add-xblock-component-menu.underscore create mode 100644 cms/templates/js/add-xblock-component.underscore create mode 100644 cms/templates/js/mock/mock-unit-page-xblock.underscore create mode 100644 cms/templates/js/mock/mock-unit-page.underscore create mode 100644 cms/templates/studio_container_wrapper.html delete mode 100644 cms/templates/studio_vertical_wrapper.html create mode 100644 common/lib/xmodule/xmodule/studio_editable.py create mode 100644 common/lib/xmodule/xmodule/tests/test_studio_editable.py create mode 100644 common/lib/xmodule/xmodule/tests/test_vertical.py create mode 100644 common/test/acceptance/pages/studio/utils.py create mode 100644 lms/templates/studio_render_children_view.html delete mode 100644 lms/templates/vert_module_studio_view.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e65666570..ba1c51ddae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ the top. Include a label indicating the component affected. Blades: Tolerance expressed in percentage now computes correctly. BLD-522. +Studio: Support add, delete and duplicate on the container page. STUD-1490. + Studio: Add drag-and-drop support to the container page. STUD-1309. Common: Add extensible third-party auth module. @@ -20,7 +22,8 @@ LMS: Switch default instructor dashboard to the new (formerly "beta") Blades: Handle situation if no response were sent from XQueue to LMS in Matlab problem after Run Code button press. BLD-994. -Blades: Set initial video quality to large instead of default to avoid automatic switch to HD when iframe resizes. BLD-981. +Blades: Set initial video quality to large instead of default to avoid automatic +switch to HD when iframe resizes. BLD-981. Blades: Add an upload button for authors to provide students with an option to download a handout associated with a video (of arbitrary file format). BLD-1000. diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 2539a4efbf..ce14c55e52 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -490,12 +490,10 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): Tests the ajax callback to render an XModule """ resp = self._test_preview(Location('i4x', 'edX', 'toy', 'vertical', 'vertical_test', None), 'container_preview') - # These are the data-ids of the xblocks contained in the vertical. - # Ultimately, these must be converted to new locators. - self.assertContains(resp, 'i4x://edX/toy/video/sample_video') - self.assertContains(resp, 'i4x://edX/toy/video/separate_file_video') - self.assertContains(resp, 'i4x://edX/toy/video/video_with_end_time') - self.assertContains(resp, 'i4x://edX/toy/poll_question/T1_changemind_poll_foo_2') + self.assertContains(resp, '/branch/draft/block/sample_video') + self.assertContains(resp, '/branch/draft/block/separate_file_video') + self.assertContains(resp, '/branch/draft/block/video_with_end_time') + self.assertContains(resp, '/branch/draft/block/T1_changemind_poll_foo_2') def _test_preview(self, location, view_name): """ Preview test case. """ diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 425b330353..6df2e407dd 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -164,70 +164,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N except ItemNotFoundError: return HttpResponseBadRequest() - component_templates = defaultdict(list) - for category in COMPONENT_TYPES: - component_class = _load_mixed_class(category) - # add the default template - # TODO: Once mixins are defined per-application, rather than per-runtime, - # this should use a cms mixed-in class. (cpennington) - if hasattr(component_class, 'display_name'): - display_name = component_class.display_name.default or 'Blank' - else: - display_name = 'Blank' - component_templates[category].append(( - display_name, - category, - False, # No defaults have markdown (hardcoded current default) - None # no boilerplate for overrides - )) - # add boilerplates - if hasattr(component_class, 'templates'): - for template in component_class.templates(): - filter_templates = getattr(component_class, 'filter_templates', None) - if not filter_templates or filter_templates(template, course): - component_templates[category].append(( - template['metadata'].get('display_name'), - category, - template['metadata'].get('markdown') is not None, - template.get('template_id') - )) - - # Check if there are any advanced modules specified in the course policy. - # These modules should be specified as a list of strings, where the strings - # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be - # enabled for the course. - course_advanced_keys = course.advanced_modules - - # Set component types according to course policy file - if isinstance(course_advanced_keys, list): - for category in course_advanced_keys: - if category in ADVANCED_COMPONENT_TYPES: - # Do I need to allow for boilerplates or just defaults on the - # class? i.e., can an advanced have more than one entry in the - # menu? one for default and others for prefilled boilerplates? - try: - component_class = _load_mixed_class(category) - - component_templates['advanced'].append( - ( - component_class.display_name.default or category, - category, - False, - None # don't override default data - ) - ) - except PluginMissingError: - # dhm: I got this once but it can happen any time the - # course author configures an advanced component which does - # not exist on the server. This code here merely - # prevents any authors from trying to instantiate the - # non-existent component type by not showing it in the menu - pass - else: - log.error( - "Improper format for course advanced keys! %s", - course_advanced_keys - ) + component_templates = _get_component_templates(course) xblocks = item.get_children() locators = [ @@ -274,7 +211,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N 'unit': item, 'unit_locator': locator, 'locators': locators, - 'component_templates': component_templates, + 'component_templates': json.dumps(component_templates), 'draft_preview_link': preview_lms_link, 'published_preview_link': lms_link, 'subsection': containing_subsection, @@ -312,6 +249,7 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g except ItemNotFoundError: return HttpResponseBadRequest() + component_templates = _get_component_templates(course) ancestor_xblocks = [] parent = get_parent_xblock(xblock) while parent and parent.category != 'sequential': @@ -329,11 +267,106 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g 'unit': unit, 'unit_publish_state': unit_publish_state, 'ancestor_xblocks': ancestor_xblocks, + 'component_templates': json.dumps(component_templates), }) else: return HttpResponseBadRequest("Only supports html requests") +def _get_component_templates(course): + """ + Returns the applicable component templates that can be used by the specified course. + """ + def create_template_dict(name, cat, boilerplate_name=None, is_common=False): + """ + Creates a component template dict. + + Parameters + display_name: the user-visible name of the component + category: the type of component (problem, html, etc.) + boilerplate_name: name of boilerplate for filling in default values. May be None. + is_common: True if "common" problem, False if "advanced". May be None, as it is only used for problems. + + """ + return { + "display_name": name, + "category": cat, + "boilerplate_name": boilerplate_name, + "is_common": is_common + } + + component_templates = [] + # The component_templates array is in the order of "advanced" (if present), followed + # by the components in the order listed in COMPONENT_TYPES. + for category in COMPONENT_TYPES: + templates_for_category = [] + component_class = _load_mixed_class(category) + # add the default template + # TODO: Once mixins are defined per-application, rather than per-runtime, + # this should use a cms mixed-in class. (cpennington) + if hasattr(component_class, 'display_name'): + display_name = component_class.display_name.default or 'Blank' + else: + display_name = 'Blank' + templates_for_category.append(create_template_dict(display_name, category)) + + # add boilerplates + if hasattr(component_class, 'templates'): + for template in component_class.templates(): + filter_templates = getattr(component_class, 'filter_templates', None) + if not filter_templates or filter_templates(template, course): + templates_for_category.append( + create_template_dict( + template['metadata'].get('display_name'), + category, + template.get('template_id'), + template['metadata'].get('markdown') is not None + ) + ) + component_templates.append({"type": category, "templates": templates_for_category}) + + # Check if there are any advanced modules specified in the course policy. + # These modules should be specified as a list of strings, where the strings + # are the names of the modules in ADVANCED_COMPONENT_TYPES that should be + # enabled for the course. + course_advanced_keys = course.advanced_modules + advanced_component_templates = {"type": "advanced", "templates": []} + # Set component types according to course policy file + if isinstance(course_advanced_keys, list): + for category in course_advanced_keys: + if category in ADVANCED_COMPONENT_TYPES: + # boilerplates not supported for advanced components + try: + component_class = _load_mixed_class(category) + + advanced_component_templates['templates'].append( + create_template_dict( + component_class.display_name.default or category, + category + ) + ) + except PluginMissingError: + # dhm: I got this once but it can happen any time the + # course author configures an advanced component which does + # not exist on the server. This code here merely + # prevents any authors from trying to instantiate the + # non-existent component type by not showing it in the menu + log.warning( + "Advanced component %s does not exist. It will not be added to the Studio new component menu.", + category + ) + pass + else: + log.error( + "Improper format for course advanced keys! %s", + course_advanced_keys + ) + if len(advanced_component_templates['templates']) > 0: + component_templates.insert(0, advanced_component_templates) + + return component_templates + + @login_required def _get_item_in_course(request, locator): """ diff --git a/cms/djangoapps/contentstore/views/helpers.py b/cms/djangoapps/contentstore/views/helpers.py index 2141ecc3c5..f2baa2377c 100644 --- a/cms/djangoapps/contentstore/views/helpers.py +++ b/cms/djangoapps/contentstore/views/helpers.py @@ -8,7 +8,9 @@ from xmodule.modulestore.django import loc_mapper, modulestore __all__ = ['edge', 'event', 'landing'] EDITING_TEMPLATES = [ - "basic-modal", "modal-button", "edit-xblock-modal", "editor-mode-button", "upload-dialog", "image-modal" + "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" ] # points to the temporary course landing page with log in and sign up @@ -57,40 +59,54 @@ def get_parent_xblock(xblock): return modulestore().get_item(parent_locations[0]) -def _xblock_has_studio_page(xblock): +def is_unit(xblock): + """ + Returns true if the specified xblock is a vertical that is treated as a unit. + A unit is a vertical that is a direct child of a sequential (aka a subsection). + """ + if xblock.category == 'vertical': + parent_xblock = get_parent_xblock(xblock) + parent_category = parent_xblock.category if parent_xblock else None + return parent_category == 'sequential' + return False + + +def xblock_has_own_studio_page(xblock): """ Returns true if the specified xblock has an associated Studio page. Most xblocks do not have their own page but are instead shown on the page of their parent. There are a few exceptions: 1. Courses - 2. Verticals + 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) 3. XBlocks with children, except for: - - subsections (aka sequential blocks) - - chapters + - sequentials (aka subsections) + - chapters (aka sections) """ category = xblock.category - if category in ('course', 'vertical'): + + if is_unit(xblock): return True + elif category == 'vertical': + parent_xblock = get_parent_xblock(xblock) + return is_unit(parent_xblock) if parent_xblock else False elif category in ('sequential', 'chapter'): return False - elif xblock.has_children: - return True - else: - return False + + # All other xblocks with children have their own page + return xblock.has_children def xblock_studio_url(xblock, course=None): """ Returns the Studio editing URL for the specified xblock. """ - if not _xblock_has_studio_page(xblock): + if not xblock_has_own_studio_page(xblock): return None category = xblock.category parent_xblock = get_parent_xblock(xblock) - if parent_xblock: - parent_category = parent_xblock.category - else: - parent_category = None + parent_category = parent_xblock.category if parent_xblock else None if category == 'course': prefix = 'course' elif category == 'vertical' and parent_category == 'sequential': diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 6916120caf..f8fbc01a3e 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -33,7 +33,7 @@ from util.string_utils import str_to_bool from ..utils import get_modulestore from .access import has_course_access -from .helpers import _xmodule_recurse +from .helpers import _xmodule_recurse, xblock_has_own_studio_page from contentstore.utils import compute_publish_state, PublishState from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES from contentstore.views.preview import get_preview_fragment @@ -193,46 +193,56 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v if 'application/json' in accept_header: store = get_modulestore(old_location) - component = store.get_item(old_location) - is_read_only = _xblock_is_read_only(component) + xblock = store.get_item(old_location) + is_read_only = _is_xblock_read_only(xblock) + container_views = ['container_preview', 'reorderable_container_child_preview'] + unit_views = ['student_view'] # 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')) + xblock.runtime.wrappers.append(partial(wrap_xblock, 'StudioRuntime')) if view_name == 'studio_view': try: - fragment = component.render('studio_view') + fragment = xblock.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) + log.debug("unable to render studio_view for %r", xblock, exc_info=True) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) # change not authored by requestor but by xblocks. - store.update_item(component, None) + store.update_item(xblock, None) - elif view_name == 'student_view' and component.has_children: + elif view_name == 'student_view' and xblock_has_own_studio_page(xblock): context = { 'runtime_type': 'studio', 'container_view': False, 'read_only': is_read_only, - 'root_xblock': component, + 'root_xblock': xblock, } # For non-leaf xblocks on the unit page, show the special rendering # which links to the new container page. html = render_to_string('container_xblock_component.html', { 'xblock_context': context, - 'xblock': component, + 'xblock': xblock, 'locator': locator, }) return JsonResponse({ 'html': html, 'resources': [], }) - elif view_name in ('student_view', 'container_preview'): - is_container_view = (view_name == 'container_preview') + elif view_name in (unit_views + container_views): + is_container_view = (view_name in container_views) + + # Determine the items to be shown as reorderable. Note that the view + # 'reorderable_container_child_preview' is only rendered for xblocks that + # are being shown in a reorderable container, so the xblock is automatically + # added to the list. + reorderable_items = set() + 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 @@ -241,10 +251,11 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v 'runtime_type': 'studio', 'container_view': is_container_view, 'read_only': is_read_only, - 'root_xblock': component, + 'root_xblock': xblock if (view_name == 'container_preview') else None, + 'reorderable_items': reorderable_items } - fragment = get_preview_fragment(request, component, context) + 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. @@ -252,7 +263,7 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v fragment.content = render_to_string('component.html', { 'xblock_context': context, 'preview': fragment.content, - 'label': component.display_name or component.scope_ids.block_type, + 'label': xblock.display_name or xblock.scope_ids.block_type, }) else: raise Http404 @@ -270,7 +281,7 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v return HttpResponse(status=406) -def _xblock_is_read_only(xblock): +def _is_xblock_read_only(xblock): """ Returns true if the specified xblock is read-only, meaning that it cannot be edited. """ @@ -411,7 +422,7 @@ def _create_item(request): metadata = {} data = None template_id = request.json.get('boilerplate') - if template_id is not None: + if template_id: clz = parent.runtime.load_block_type(category) if clz is not None: template = clz.get_template(template_id) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 239accb4a6..b2847f5e20 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -28,7 +28,7 @@ from util.sandboxing import can_execute_unsafe_code import static_replace from .session_kv_store import SessionKeyValueStore -from .helpers import render_from_lms +from .helpers import render_from_lms, xblock_has_own_studio_page from ..utils import get_course_for_item from contentstore.views.access import get_user_role @@ -166,6 +166,13 @@ def _load_preview_module(request, descriptor): return descriptor +def _is_xblock_reorderable(xblock, context): + """ + Returns true if the specified xblock is in the set of reorderable xblocks. + """ + return xblock.location in context['reorderable_items'] + + # pylint: disable=unused-argument def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): """ @@ -173,17 +180,21 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): """ # 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 == 'student_view': + root_xblock = context.get('root_xblock') + is_root = root_xblock and xblock.location == root_xblock.location locator = loc_mapper().translate_location(xblock.course_id, xblock.location, published=False) + is_reorderable = _is_xblock_reorderable(xblock, context) template_context = { 'xblock_context': context, 'xblock': xblock, 'locator': locator, 'content': frag.content, + 'is_root': is_root, + 'is_reorderable': is_reorderable, } - if xblock.category == 'vertical': - template = 'studio_vertical_wrapper.html' - elif xblock.location != context.get('root_xblock').location and xblock.has_children: - template = 'container_xblock_component.html' + # For child xblocks with their own page, render a link to the page + if xblock_has_own_studio_page(xblock) and not is_root: + template = 'studio_container_wrapper.html' else: template = 'studio_xblock_wrapper.html' html = render_to_string(template, template_context) diff --git a/cms/djangoapps/contentstore/views/tests/test_container.py b/cms/djangoapps/contentstore/views/tests/test_container.py deleted file mode 100644 index 0310a9544b..0000000000 --- a/cms/djangoapps/contentstore/views/tests/test_container.py +++ /dev/null @@ -1,154 +0,0 @@ -""" -Unit tests for the container view. -""" - -import json - -from contentstore.tests.utils import CourseTestCase -from contentstore.utils import compute_publish_state, PublishState -from contentstore.views.helpers import xblock_studio_url -from xmodule.modulestore.django import loc_mapper, modulestore -from xmodule.modulestore.tests.factories import ItemFactory - - -class ContainerViewTestCase(CourseTestCase): - """ - Unit tests for the container view. - """ - - def setUp(self): - super(ContainerViewTestCase, self).setUp() - self.chapter = ItemFactory.create(parent_location=self.course.location, - category='chapter', display_name="Week 1") - self.sequential = ItemFactory.create(parent_location=self.chapter.location, - category='sequential', display_name="Lesson 1") - self.vertical = ItemFactory.create(parent_location=self.sequential.location, - category='vertical', display_name='Unit') - self.child_vertical = ItemFactory.create(parent_location=self.vertical.location, - category='vertical', display_name='Child Vertical') - self.video = ItemFactory.create(parent_location=self.child_vertical.location, - category="video", display_name="My Video") - - def test_container_html(self): - branch_name = "MITx.999.Robot_Super_Course/branch/draft/block" - self._test_html_content( - self.child_vertical, - branch_name=branch_name, - expected_section_tag=( - '