From b1eccdf2d45df185af04e2d3c1247c4702c0f707 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Mon, 16 Jun 2014 14:21:42 -0400 Subject: [PATCH] Replace unit page with the container page. STUD-1754 --- .../contentstore/features/common.py | 24 - .../contentstore/features/component.py | 15 +- .../component_settings_editor_helpers.py | 4 +- .../contentstore/features/course-export.py | 2 +- .../features/problem-editor.feature | 32 - .../contentstore/features/problem-editor.py | 4 +- .../contentstore/tests/test_contentstore.py | 8 +- .../contentstore/views/component.py | 122 +-- cms/djangoapps/contentstore/views/helpers.py | 43 +- .../contentstore/views/import_export.py | 2 +- cms/djangoapps/contentstore/views/item.py | 45 +- cms/djangoapps/contentstore/views/preview.py | 4 +- .../views/tests/test_container_page.py | 71 +- .../contentstore/views/tests/test_helpers.py | 30 +- .../views/tests/test_import_export.py | 4 +- .../contentstore/views/tests/test_preview.py | 6 +- .../views/tests/test_unit_page.py | 27 +- .../contentstore/views/tests/utils.py | 16 +- cms/static/coffee/spec/main.coffee | 1 - cms/static/coffee/src/views/unit.coffee | 273 ------ cms/static/js/base.js | 2 +- cms/static/js/spec/views/container_spec.js | 20 +- .../js/spec/views/pages/container_spec.js | 107 ++- cms/static/js/spec/views/unit_spec.js | 272 ------ .../js/spec/views/xblock_editor_spec.js | 2 +- cms/static/js/spec_helpers/edit_helpers.js | 4 +- cms/static/js/spec_helpers/view_helpers.js | 6 +- cms/static/js/views/modals/edit_xblock.js | 18 +- cms/static/js/views/pages/container.js | 22 +- cms/static/js/views/xblock_editor.js | 26 +- .../js/views/xblock_string_field_editor.js | 101 ++ cms/static/sass/_base.scss | 13 +- cms/static/sass/_shame.scss | 11 +- cms/static/sass/views/_container.scss | 169 +++- cms/templates/container.html | 102 +- cms/templates/container_xblock_component.html | 47 - .../js/mock/mock-container-page.underscore | 14 +- .../js/mock/mock-container-xblock.underscore | 6 +- .../mock-empty-container-xblock.underscore | 2 +- .../mock-unit-page-child-container.underscore | 37 - .../js/mock/mock-unit-page-xblock.underscore | 27 - .../mock-updated-container-xblock.underscore | 2 +- ...lock-editor-with-custom-buttons.underscore | 2 +- .../js/mock/mock-xblock-editor.underscore | 2 +- ...xmodule-editor-with-custom-tabs.underscore | 2 +- .../js/mock/mock-xmodule-editor.underscore | 2 +- ...ck-xmodule-settings-only-editor.underscore | 2 +- .../js/xblock-string-field-editor.underscore | 3 + cms/templates/studio_vertical_wrapper.html | 24 - cms/templates/studio_xblock_wrapper.html | 9 +- cms/templates/unit.html | 149 --- cms/templates/ux/reference/container.html | 2 +- cms/templates/ux/reference/unit.html | 887 ------------------ cms/templates/widgets/units.html | 5 +- cms/urls.py | 1 - common/djangoapps/terrain/ui_helpers.py | 5 - common/djangoapps/xmodule_modifiers.py | 3 +- .../xmodule/tests/test_split_test_module.py | 1 - .../xmodule/tests/test_studio_editable.py | 1 - .../xmodule/xmodule/tests/test_vertical.py | 4 +- common/lib/xmodule/xmodule/vertical_module.py | 8 +- .../test/acceptance/pages/studio/container.py | 44 +- .../test/acceptance/pages/studio/overview.py | 8 +- common/test/acceptance/pages/studio/unit.py | 194 ---- .../tests/test_studio_acid_xblock.py | 13 +- .../acceptance/tests/test_studio_container.py | 34 +- .../tests/test_studio_split_test.py | 18 +- 67 files changed, 712 insertions(+), 2454 deletions(-) delete mode 100644 cms/static/js/spec/views/unit_spec.js create mode 100644 cms/static/js/views/xblock_string_field_editor.js delete mode 100644 cms/templates/container_xblock_component.html delete mode 100644 cms/templates/js/mock/mock-unit-page-child-container.underscore delete mode 100644 cms/templates/js/mock/mock-unit-page-xblock.underscore create mode 100644 cms/templates/js/xblock-string-field-editor.underscore delete mode 100644 cms/templates/studio_vertical_wrapper.html delete mode 100644 cms/templates/unit.html delete mode 100644 cms/templates/ux/reference/unit.html delete mode 100644 common/test/acceptance/pages/studio/unit.py diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 0370504de3..c22eb92083 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -397,27 +397,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 05598065b5..ea1cee8d62 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 see_component_in_position(step, display_name, index): @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 a4d777d20e..e440287c2c 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 cdc6ea13bf..56dc5f66bc 100644 --- a/cms/djangoapps/contentstore/features/course-export.py +++ b/cms/djangoapps/contentstore/features/course-export.py @@ -55,7 +55,7 @@ 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", None) - vertical_url = reverse_usage_url('unit_handler', vertical_usage_key) + vertical_url = reverse_usage_url('container_handler', vertical_usage_key) # Remove the trailing "/None" from the URL - we don't know the course ID, so we just want to # check that we visited a vertical URL. if vertical_url.endswith("/None"): diff --git a/cms/djangoapps/contentstore/features/problem-editor.feature b/cms/djangoapps/contentstore/features/problem-editor.feature index d947493ad1..4b3292665c 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 5a7adca76c..44647b5eeb 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 101533a467..fe17d849c6 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -95,7 +95,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): # 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) for expected in expected_types: @@ -120,7 +120,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): # 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) def check_edit_unit(self, test_course_name): @@ -926,7 +926,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): # 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) @@ -1293,7 +1293,7 @@ class ContentStoreTest(ContentStoreTestCase): # 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) def delete_item(category, name): diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 2ff40a5d94..5e7f621eb3 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -11,7 +11,6 @@ from django.conf import settings 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 xmodule.modulestore import PublishState @@ -23,7 +22,7 @@ from xblock.plugin import PluginMissingError from xblock.runtime import Mixologist from contentstore.utils import get_lms_link_for_item, compute_publish_state -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 @@ -34,14 +33,13 @@ from django.utils.translation import ugettext as _ __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'] # Constants for determining if these components should be enabled for this course @@ -135,84 +133,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': item.location, - '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 @@ -235,25 +155,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': xblock.location, - '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): @@ -285,16 +217,6 @@ def get_component_templates(course): '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 @@ -305,7 +227,7 @@ def get_component_templates(course): # 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) @@ -328,7 +250,7 @@ def get_component_templates(course): 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) @@ -350,7 +272,7 @@ def get_component_templates(course): 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, default_display_name=category) + component_display_name = xblock_type_display_name(category, default_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 f24d4828fd..6b83c2de08 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 @@ __all__ = ['edge', 'event', 'landing'] 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 @@ -70,8 +75,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) @@ -83,7 +88,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 @@ -97,12 +102,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 3b845a6611..826165de5a 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -348,7 +348,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 5373915c5e..cca0c8e0fe 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -21,18 +21,16 @@ from xblock.fragment import Fragment import xmodule from xmodule.tabs import StaticTab, CourseTabList -from xmodule.modulestore import PublishState, ModuleStoreEnum +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore -from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES 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 .access import has_course_access -from .helpers import xblock_has_own_studio_page -from contentstore.utils import compute_publish_state +from contentstore.views.helpers import is_unit from contentstore.views.preview import get_preview_fragment from edxmako.shortcuts import render_to_string from models.settings.course_grading import CourseGradingModel @@ -187,7 +185,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 @@ -204,8 +201,8 @@ def xblock_view_handler(request, usage_key_string, view_name): fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) store.update_item(xblock, request.user.id) - 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 @@ -215,27 +212,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, @@ -263,10 +254,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(user, 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 6404864587..23e0ba9fa8 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -191,8 +191,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 5d9132e4f4..b6f5cb5424 100644 --- a/cms/djangoapps/contentstore/views/tests/test_container_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_container_page.py @@ -3,9 +3,7 @@ Unit tests for the container page. """ import re -from contentstore.utils import compute_publish_state from contentstore.views.tests.utils import StudioPageTestCase -from xmodule.modulestore import PublishState from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory @@ -35,10 +33,13 @@ class ContainerPageTestCase(StudioPageTestCase): '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): @@ -57,15 +58,15 @@ class ContainerPageTestCase(StudioPageTestCase): 'data-locator="{0}" data-course-key="{0.course_key}">'.format(draft_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 draft version of the container @@ -82,19 +83,9 @@ class ContainerPageTestCase(StudioPageTestCase): 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. - 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. @@ -102,23 +93,17 @@ class ContainerPageTestCase(StudioPageTestCase): published_unit = self.store.publish(self.vertical.location, self.user.id) published_child_container = self.store.get_item(self.child_container.location) published_child_vertical = self.store.get_item(self.child_vertical.location) - self.validate_preview_html(published_unit, self.container_view, - can_edit=False, can_reorder=False, can_add=False) - self.validate_preview_html(published_child_container, self.container_view, - can_edit=False, can_reorder=False, can_add=False) - self.validate_preview_html(published_child_vertical, self.reorderable_child_view, - can_edit=False, can_reorder=False, can_add=False) + self.validate_preview_html(published_unit, self.container_view) + self.validate_preview_html(published_child_container, self.container_view) + self.validate_preview_html(published_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. """ - self.validate_preview_html(self.vertical, self.container_view, - can_edit=True, can_reorder=True, can_add=True) - self.validate_preview_html(self.child_container, self.container_view, - can_edit=True, can_reorder=True, can_add=True) - self.validate_preview_html(self.child_vertical, self.reorderable_child_view, - can_edit=True, can_reorder=True, can_add=True) + 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_public_child_container_preview_html(self): """ @@ -126,25 +111,11 @@ class ContainerPageTestCase(StudioPageTestCase): """ empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test') published_empty_child_container = self.store.publish(empty_child_container.location, self.user.id) - self.validate_preview_html(published_empty_child_container, self.reorderable_child_view, - can_reorder=False, can_edit=False, can_add=False) + self.validate_preview_html(published_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. """ empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test') - self.validate_preview_html(empty_child_container, self.reorderable_child_view, - can_reorder=True, can_edit=True, can_add=False) - - def _create_item(self, parent_location, category, display_name, **kwargs): - """ - creates an item in the module store, without publishing it. - """ - return ItemFactory.create( - parent_location=parent_location, - category=category, - display_name=display_name, - publish_item=False, - **kwargs - ) + self.validate_preview_html(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 6acc04ed7b..f1eda99789 100644 --- a/cms/djangoapps/contentstore/views/tests/test_helpers.py +++ b/cms/djangoapps/contentstore/views/tests/test_helpers.py @@ -3,7 +3,7 @@ Unit tests for helpers.py. """ 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 @@ class HelpersTestCase(CourseTestCase): # 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/i4x://MITx/999/vertical/Unit') + u'/container/i4x://MITx/999/vertical/Unit') # Verify child vertical URL child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical', @@ -43,3 +45,23 @@ class HelpersTestCase(CourseTestCase): 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 616888babd..161c489250 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -293,7 +293,7 @@ class ExportTestCase(CourseTestCase): """ fake_xblock = ItemFactory.create(parent_location=self.course.location, category='aawefawef') self.store.publish(fake_xblock.location, self.user.id) - self._verify_export_failure(u'/unit/i4x://MITx/999/course/Robot_Super_Course') + self._verify_export_failure(u'/container/i4x://MITx/999/course/Robot_Super_Course') def test_export_failure_subsection_level(self): """ @@ -305,7 +305,7 @@ class ExportTestCase(CourseTestCase): category='aawefawef' ) - self._verify_export_failure(u'/unit/i4x://MITx/999/vertical/foo') + self._verify_export_failure(u'/container/i4x://MITx/999/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 e1c18711bd..ee26693d4e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_preview.py +++ b/cms/djangoapps/contentstore/views/tests/test_preview.py @@ -38,7 +38,11 @@ class GetPreviewHtmlTestCase(TestCase): 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 967d6628aa..f25ddfba6e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_unit_page.py +++ b/cms/djangoapps/contentstore/views/tests/test_unit_page.py @@ -21,35 +21,18 @@ class UnitPageTestCase(StudioPageTestCase): category="video", display_name="My Video") self.store = modulestore() - 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. - """ - html = self.get_page_html(self.vertical) - 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. """ published_video = self.store.publish(self.video.location, self.user.id) - 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): """ Verify that a draft 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_public_child_container_preview_html(self): """ @@ -61,8 +44,7 @@ class UnitPageTestCase(StudioPageTestCase): ItemFactory.create(parent_location=child_container.location, category='html', display_name='grandchild') published_child_container = self.store.publish(child_container.location, self.user.id) - self.validate_preview_html(published_child_container, STUDENT_VIEW, - can_reorder=True, can_edit=True, can_add=False) + self.validate_preview_html(published_child_container, STUDENT_VIEW, can_add=False) def test_draft_child_container_preview_html(self): """ @@ -74,5 +56,4 @@ class UnitPageTestCase(StudioPageTestCase): ItemFactory.create(parent_location=child_container.location, category='html', display_name='grandchild') draft_child_container = self.store.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 046465e35a..094a789214 100644 --- a/cms/djangoapps/contentstore/views/tests/utils.py +++ b/cms/djangoapps/contentstore/views/tests/utils.py @@ -41,19 +41,16 @@ class StudioPageTestCase(CourseTestCase): 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 @@ class StudioPageTestCase(CourseTestCase): '' ] 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 46b039956c..50935e2924 100644 --- a/cms/static/coffee/spec/main.coffee +++ b/cms/static/coffee/spec/main.coffee @@ -225,7 +225,6 @@ define([ "js/spec/views/group_configuration_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 index 4aa2726773..e69de29bb2 100644 --- a/cms/static/coffee/src/views/unit.coffee +++ b/cms/static/coffee/src/views/unit.coffee @@ -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() - }).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 8b84c6cc07..7b9c69ee21 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 c61d3bd114..7f57eb1e91 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 35cb2e4579..a5a7796769 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); + }); + + 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); + }); + + 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() { - 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(); }); @@ -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 2464fa2653..0000000000 --- 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 d9af3e32ec..8b771e2fb4 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 869949b39f..d59abb3c11 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 58ad26627d..2674d985a5 100644 --- a/cms/static/js/spec_helpers/view_helpers.js +++ b/cms/static/js/spec_helpers/view_helpers.js @@ -9,9 +9,11 @@ define(['jquery', 'js/views/feedback_notification', 'js/views/feedback_prompt'], verifyNotificationHidden, createPromptSpy, confirmPrompt, verifyPromptShowing, verifyPromptHidden; - 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 1e0cad0cb3..898759039b 100644 --- a/cms/templates/ux/reference/container.html +++ b/cms/templates/ux/reference/container.html @@ -108,7 +108,7 @@ from django.core.urlresolvers import reverse
-
+

Video

diff --git a/cms/templates/ux/reference/unit.html b/cms/templates/ux/reference/unit.html deleted file mode 100644 index 9ad1ee32c0..0000000000 --- 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 c7a0d2e0e9..04777a21d3 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 %>