diff --git a/cms/djangoapps/contentstore/features/common.py b/cms/djangoapps/contentstore/features/common.py index 76c83b9e01b2..9aaa4f651605 100644 --- a/cms/djangoapps/contentstore/features/common.py +++ b/cms/djangoapps/contentstore/features/common.py @@ -203,8 +203,8 @@ def create_a_course(): def add_section(): - world.css_click('.course-outline .add-button') - assert_true(world.is_css_present('.outline-item-section .xblock-field-value')) + world.css_click('.outline .button-new') + assert_true(world.is_css_present('.outline-section .xblock-field-value')) def set_date_and_time(date_css, desired_date, time_css, desired_time, key=None): @@ -241,7 +241,7 @@ def create_unit_from_course_outline(): The end result is the page where the user is editing the new unit. """ css_selectors = [ - '.outline-item-subsection .expand-collapse', '.outline-item-subsection .add-button' + '.outline-subsection .expand-collapse', '.outline-subsection .button-new' ] for selector in css_selectors: world.css_click(selector) diff --git a/cms/djangoapps/contentstore/features/course-outline.py b/cms/djangoapps/contentstore/features/course-outline.py index f3203880aacb..8786cfcaf11f 100644 --- a/cms/djangoapps/contentstore/features/course-outline.py +++ b/cms/djangoapps/contentstore/features/course-outline.py @@ -69,7 +69,7 @@ def i_add_a_section(step): @step(u'I press the "section" delete icon') def i_press_the_section_delete_icon(step): - delete_locator = 'section .outline-item-section > .wrapper-xblock-header a.delete-button' + delete_locator = 'section .outline-section > .section-header a.delete-button' world.css_click(delete_locator) @@ -82,27 +82,27 @@ def i_confirm_all_alerts(step): @step(u'I see the "([^"]*) All Sections" link$') def i_see_the_collapse_expand_all_span(step, text): if text == "Collapse": - span_locator = '.toggle-button-expand-collapse .collapse-all .label' + span_locator = '.button-toggle-expand-collapse .collapse-all .label' elif text == "Expand": - span_locator = '.toggle-button-expand-collapse .expand-all .label' + span_locator = '.button-toggle-expand-collapse .expand-all .label' assert_true(world.css_visible(span_locator)) @step(u'I do not see the "([^"]*) All Sections" link$') def i_do_not_see_the_collapse_expand_all_span(step, text): if text == "Collapse": - span_locator = '.toggle-button-expand-collapse .collapse-all .label' + span_locator = '.button-toggle-expand-collapse .collapse-all .label' elif text == "Expand": - span_locator = '.toggle-button-expand-collapse .expand-all .label' + span_locator = '.button-toggle-expand-collapse .expand-all .label' assert_false(world.css_visible(span_locator)) @step(u'I click the "([^"]*) All Sections" link$') def i_click_the_collapse_expand_all_span(step, text): if text == "Collapse": - span_locator = '.toggle-button-expand-collapse .collapse-all .label' + span_locator = '.button-toggle-expand-collapse .collapse-all .label' elif text == "Expand": - span_locator = '.toggle-button-expand-collapse .expand-all .label' + span_locator = '.button-toggle-expand-collapse .expand-all .label' assert_true(world.browser.is_element_present_by_css(span_locator)) world.css_click(span_locator) @@ -110,9 +110,9 @@ def i_click_the_collapse_expand_all_span(step, text): @step(u'I ([^"]*) the first section$') def i_collapse_expand_a_section(step, text): if text == "collapse": - locator = 'section .outline-item-section .ui-toggle-expansion' + locator = 'section .outline-section .ui-toggle-expansion' elif text == "expand": - locator = 'section .outline-item-section .ui-toggle-expansion' + locator = 'section .outline-section .ui-toggle-expansion' world.css_click(locator) diff --git a/cms/djangoapps/contentstore/features/courses.py b/cms/djangoapps/contentstore/features/courses.py index 6425d1c4c5cc..8f8bc2a1e8f6 100644 --- a/cms/djangoapps/contentstore/features/courses.py +++ b/cms/djangoapps/contentstore/features/courses.py @@ -66,5 +66,5 @@ def i_am_on_tab(step, tab_name): @step('I see a link for adding a new section$') def i_see_new_section_link(step): - link_css = '.course-outline .add-button' + link_css = '.outline .button-new' assert world.css_has_text(link_css, 'New Section') diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 3b254e3dacb5..bf422c57d12d 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1208,7 +1208,7 @@ def test_course_overview_view_with_course(self): resp = self._show_course_overview(course.id) self.assertContains( resp, - '
'.format( + '
'.format( locator='i4x://MITx/999/course/Robot_Super_Course', course_key='MITx/999/Robot_Super_Course', ), diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 72ffd9f911c4..43de27093ec8 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -113,21 +113,7 @@ def course_image_url(course): path = loc.to_deprecated_string() return path - -def compute_publish_state(xblock): - """ - Returns whether this xblock is draft, public, or private. - - Returns: - PublishState.draft - content is in the process of being edited, but still has a previous - version deployed to LMS - PublishState.public - content is locked and deployed to LMS - PublishState.private - content is editable and not deployed to LMS - """ - - return modulestore().compute_publish_state(xblock) - - +# pylint: disable=invalid-name def is_currently_visible_to_students(xblock): """ Returns true if there is a published version of the xblock that is currently visible to students. diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 8f97fb9f2e3e..a9773d20e03f 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -11,8 +11,8 @@ from xmodule.modulestore.exceptions import ItemNotFoundError from edxmako.shortcuts import render_to_response +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore -from xmodule.modulestore import PublishState from xblock.core import XBlock from xblock.django.request import webob_to_django_response, django_to_webob_request @@ -21,7 +21,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.utils import get_lms_link_for_item from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name from contentstore.views.item import create_xblock_info @@ -122,8 +122,8 @@ def subsection_handler(request, usage_key_string): can_view_live = False subsection_units = item.get_children() for unit in subsection_units: - state = compute_publish_state(unit) - if state in (PublishState.public, PublishState.draft): + has_published = modulestore().has_item(unit.location, revision=ModuleStoreEnum.RevisionOption.published_only) + if has_published: can_view_live = True break @@ -198,7 +198,7 @@ def container_handler(request, usage_key_string): # Fetch the XBlock info for use by the container page. Note that it includes information # about the block's ancestors and siblings for use by the Unit Outline. - xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page) + xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, include_edited_by=True, include_published_by=True) # Create the link for preview. preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE') diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 462e30076178..a7d20a858699 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -282,7 +282,7 @@ def find_xblock_info(xblock_info, locator): """ if xblock_info['id'] == locator: return xblock_info - children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None + children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None if children: for child_xblock_info in children: result = find_xblock_info(child_xblock_info, locator) @@ -295,7 +295,7 @@ def collect_all_locators(locators, xblock_info): Collect all the locators for an xblock and its children. """ locators.append(xblock_info['id']) - children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None + children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None if children: for child_xblock_info in children: collect_all_locators(locators, child_xblock_info) diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 5751d2e210eb..b892635e31fd 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -586,7 +586,7 @@ def _get_module_info(xblock, rewrite_static_links=True): def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, - include_children_predicate=NEVER): + include_edited_by=False, include_published_by=False, include_children_predicate=NEVER): """ Creates the information needed for client-side XBlockInfo. @@ -600,10 +600,6 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F In addition, an optional include_children_predicate argument can be provided to define whether or not a particular xblock should have its children included. """ - published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only) - - # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set - release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None def safe_get_username(user_id): """ @@ -622,22 +618,35 @@ def safe_get_username(user_id): return None + # Compute the child info first so it can be included in aggregate information for the parent + if include_child_info and xblock.has_children: + child_info = _create_xblock_child_info( + xblock, include_children_predicate=include_children_predicate + ) + else: + child_info = None + + # Treat DEFAULT_START_DATE as a magic number that means the release date has not been set + release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None + published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only) + currently_visible_to_students = is_currently_visible_to_students(xblock) + + is_xblock_unit = is_unit(xblock) + is_unit_with_changes = is_xblock_unit and modulestore().has_changes(xblock.location) + xblock_info = { "id": unicode(xblock.location), "display_name": xblock.display_name_with_default, "category": xblock.category, - "has_changes": modulestore().has_changes(xblock.location), - "published": published, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, - "edited_by": safe_get_username(xblock.subtree_edited_by), + "published": published, "published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, - "published_by": safe_get_username(xblock.published_by), 'studio_url': xblock_studio_url(xblock), "released_to_students": datetime.now(UTC) > xblock.start, "release_date": release_date, "release_date_from": _get_release_date_from(xblock) if release_date else None, - "visible_to_staff_only": xblock.visible_to_staff_only, - "currently_visible_to_students": is_currently_visible_to_students(xblock), + "currently_visible_to_students": currently_visible_to_students, + "visibility_state": _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None } if data is not None: xblock_info["data"] = data @@ -645,13 +654,91 @@ def safe_get_username(user_id): xblock_info["metadata"] = metadata if include_ancestor_info: xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock) - if include_child_info and xblock.has_children: - xblock_info['child_info'] = _create_xblock_child_info( - xblock, include_children_predicate=include_children_predicate - ) + if child_info: + xblock_info['child_info'] = child_info + # Currently, 'edited_by' and 'published_by' are only used by the container page. Only compute them when asked to do + # so, since safe_get_username() is expensive. + if include_edited_by: + xblock_info['edited_by'] = safe_get_username(xblock.subtree_edited_by) + if include_published_by: + xblock_info['published_by'] = safe_get_username(xblock.published_by) + # On the unit page only, add 'has_changes' to indicate when there are changes that can be discarded. + # We don't add it in general because it is an expensive operation. + if is_xblock_unit: + xblock_info['has_changes'] = is_unit_with_changes + return xblock_info +class VisibilityState(object): + """ + Represents the possible visibility states for an xblock: + + live - the block and all of its descendants are live to students (excluding staff only items) + Note: Live means both published and released. + + ready - the block is ready to go live and all of its descendants are live or ready (excluding staff only items) + Note: content is ready when it is published and scheduled with a release date in the future. + + unscheduled - the block and all of its descendants have no release date (excluding staff only items) + Note: it is valid for items to be published with no release date in which case they are still unscheduled. + + needs_attention - the block or its descendants are not fully live, ready or unscheduled (excluding staff only items) + For example: one subsection has draft content, or there's both unreleased and released content in one section. + + staff_only - all of the block's content is to be shown to staff only + Note: staff only items do not affect their parent's state. + """ + live = 'live' + ready = 'ready' + unscheduled = 'unscheduled' + needs_attention = 'needs_attention' + staff_only = 'staff_only' + + +def _compute_visibility_state(xblock, child_info, is_unit_with_changes): + """ + Returns the current publish state for the specified xblock and its children + """ + if xblock.visible_to_staff_only: + return VisibilityState.staff_only + elif is_unit_with_changes: + # Note that a unit that has never been published will fall into this category, + # as well as previously published units with draft content. + return VisibilityState.needs_attention + is_unscheduled = xblock.start == DEFAULT_START_DATE + is_live = datetime.now(UTC) > xblock.start + children = child_info and child_info['children'] + if children and len(children) > 0: + all_staff_only = True + all_unscheduled = True + all_live = True + for child in child_info['children']: + child_state = child['visibility_state'] + if child_state == VisibilityState.needs_attention: + return child_state + elif not child_state == VisibilityState.staff_only: + all_staff_only = False + if not child_state == VisibilityState.unscheduled: + all_unscheduled = False + if not child_state == VisibilityState.live: + all_live = False + if all_staff_only: + return VisibilityState.staff_only + elif all_unscheduled: + return VisibilityState.unscheduled if is_unscheduled else VisibilityState.needs_attention + elif all_live: + return VisibilityState.live if is_live else VisibilityState.needs_attention + else: + return VisibilityState.ready if not is_unscheduled else VisibilityState.needs_attention + if is_unscheduled: + return VisibilityState.unscheduled + elif is_live: + return VisibilityState.live + else: + return VisibilityState.ready + + def _create_xblock_ancestor_info(xblock): """ Returns information about the ancestors of an xblock. Note that the direct parent will also return diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index fde16b5c0b8b..68729b3d657c 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -9,7 +9,7 @@ from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url from contentstore.views.course import course_outline_initial_state -from contentstore.views.item import create_xblock_info +from contentstore.views.item import create_xblock_info, VisibilityState from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -123,6 +123,7 @@ def test_json_responses(self): self.assertEqual(json_response['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(json_response['display_name'], 'Robot Super Course') self.assertTrue(json_response['published']) + self.assertIsNone(json_response['visibility_state']) # Now verify the first child children = json_response['child_info']['children'] @@ -131,7 +132,8 @@ def test_json_responses(self): self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['id'], 'i4x://MITx/999/chapter/Week_1') self.assertEqual(first_child_response['display_name'], 'Week 1') - self.assertTrue(first_child_response['published']) + self.assertTrue(json_response['published']) + self.assertEqual(first_child_response['visibility_state'], VisibilityState.unscheduled) self.assertTrue(len(first_child_response['child_info']['children']) > 0) # Finally, validate the entire response for consistency @@ -144,7 +146,7 @@ def assert_correct_json_response(self, json_response): self.assertIsNotNone(json_response['display_name']) self.assertIsNotNone(json_response['id']) self.assertIsNotNone(json_response['category']) - self.assertIsNotNone(json_response['published']) + self.assertTrue(json_response['published']) if json_response.get('child_info', None): for child_response in json_response['child_info']['children']: self.assert_correct_json_response(child_response) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 85d251f468a4..d17ab7eaafbc 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -1,7 +1,7 @@ """Tests for items views.""" import json -from datetime import datetime +from datetime import datetime, timedelta import ddt from mock import patch @@ -21,11 +21,10 @@ SPLIT_TEST_COMPONENT_TYPE ) -from contentstore.views.item import create_xblock_info, ALWAYS +from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState from contentstore.tests.utils import CourseTestCase from student.tests.factories import UserFactory from xmodule.capa_module import CapaDescriptor -from xmodule.modulestore import PublishState from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.tests.factories import ItemFactory @@ -417,18 +416,6 @@ def setUp(self): self.course_update_url = reverse_usage_url("xblock_handler", self.usage_key) - def verify_publish_state(self, usage_key, expected_publish_state): - """ - Helper method that gets the item from the module store and verifies that the publish state is as expected. - Returns the item corresponding to the given usage_key. - """ - item = self.get_item_from_modulestore( - usage_key, - (expected_publish_state == PublishState.private) or (expected_publish_state == PublishState.draft) - ) - self.assertEqual(expected_publish_state, self.store.compute_publish_state(item)) - return item - def test_delete_field(self): """ Sending null in for a field 'deletes' it @@ -534,15 +521,35 @@ def test_reorder_children(self): self.assertEqual(unit1_usage_key, children[2]) self.assertEqual(unit2_usage_key, children[1]) + def _is_location_published(self, location): + """ + Returns whether or not the item with given location has a published version. + """ + return modulestore().has_item(location, revision=ModuleStoreEnum.RevisionOption.published_only) + + def _verify_published_with_no_draft(self, location): + """ + Verifies the item with given location has a published version and no draft (unpublished changes). + """ + self.assertTrue(self._is_location_published(location)) + self.assertFalse(modulestore().has_changes(location)) + + def _verify_published_with_draft(self, location): + """ + Verifies the item with given location has a published version and also a draft version (unpublished changes). + """ + self.assertTrue(self._is_location_published(location)) + self.assertTrue(modulestore().has_changes(location)) + def test_make_public(self): """ Test making a private problem public (publishing it). """ # When the problem is first created, it is only in draft (because of its category). - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.assertFalse(self._is_location_published(self.problem_usage_key)) self.client.ajax_post( self.problem_update_url, data={'publish': 'make_public'} ) - self.verify_publish_state(self.problem_usage_key, PublishState.public) + self._verify_published_with_no_draft(self.problem_usage_key) def test_make_draft(self): """ Test creating a draft version of a public problem. """ @@ -555,19 +562,16 @@ def test_revert_to_published(self): self.problem_update_url, data={'publish': 'discard_changes'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + self._verify_published_with_no_draft(self.problem_usage_key) + published = modulestore().get_item(self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only) self.assertIsNone(published.due) def test_republish(self): """ Test republishing an item. """ new_display_name = 'New Display Name' - republish_data = { - 'publish': 'republish', - 'display_name': new_display_name - } # When the problem is first created, it is only in draft (because of its category). - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.assertFalse(self._is_location_published(self.problem_usage_key)) # Republishing when only in draft will update the draft but not cause a public item to be created. self.client.ajax_post( @@ -579,7 +583,7 @@ def test_republish(self): } } ) - self.verify_publish_state(self.problem_usage_key, PublishState.private) + self.assertFalse(self._is_location_published(self.problem_usage_key)) draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True) self.assertEqual(draft.display_name, new_display_name) @@ -600,7 +604,7 @@ def test_republish(self): } } ) - self.verify_publish_state(self.problem_usage_key, PublishState.public) + self._verify_published_with_no_draft(self.problem_usage_key) published = modulestore().get_item( self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only @@ -616,7 +620,8 @@ def _make_draft_content_different_from_published(self): self.problem_update_url, data={'publish': 'make_public'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + self._verify_published_with_no_draft(self.problem_usage_key) + published = modulestore().get_item(self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only) # Update the draft version and check that published is different. self.client.ajax_post( @@ -650,7 +655,8 @@ def test_published_and_draft_contents_with_update(self): self.problem_update_url, data={'publish': 'make_public'} ) - published = self.verify_publish_state(self.problem_usage_key, PublishState.public) + self._verify_published_with_no_draft(self.problem_usage_key) + published = modulestore().get_item(self.problem_usage_key, revision=ModuleStoreEnum.RevisionOption.published_only) # Now make a draft self.client.ajax_post( @@ -695,8 +701,8 @@ def test_publish_states_of_nested_xblocks(self): # The unit and its children should be private initially unit_update_url = reverse_usage_url('xblock_handler', unit_usage_key) - self.verify_publish_state(unit_usage_key, PublishState.private) - self.verify_publish_state(html_usage_key, PublishState.private) + self.assertFalse(self._is_location_published(unit_usage_key)) + self.assertFalse(self._is_location_published(html_usage_key)) # Make the unit public and verify that the problem is also made public resp = self.client.ajax_post( @@ -704,8 +710,8 @@ def test_publish_states_of_nested_xblocks(self): data={'publish': 'make_public'} ) self.assertEqual(resp.status_code, 200) - self.verify_publish_state(unit_usage_key, PublishState.public) - self.verify_publish_state(html_usage_key, PublishState.public) + self._verify_published_with_no_draft(unit_usage_key) + self._verify_published_with_no_draft(html_usage_key) # Make a draft for the unit and verify that the problem also has a draft resp = self.client.ajax_post( @@ -716,8 +722,8 @@ def test_publish_states_of_nested_xblocks(self): } ) self.assertEqual(resp.status_code, 200) - self.verify_publish_state(unit_usage_key, PublishState.draft) - self.verify_publish_state(html_usage_key, PublishState.draft) + self._verify_published_with_draft(unit_usage_key) + self._verify_published_with_draft(html_usage_key) @skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature') @@ -1132,6 +1138,7 @@ def test_vertical_xblock_info(self): xblock_info = create_xblock_info( vertical, include_child_info=True, + include_edited_by=True, include_children_predicate=ALWAYS, include_ancestor_info=True ) @@ -1166,7 +1173,6 @@ def validate_chapter_xblock_info(self, xblock_info, has_child_info=True): self.assertEqual(xblock_info['id'], 'i4x://MITx/999/chapter/Week_1') self.assertEqual(xblock_info['display_name'], 'Week 1') self.assertTrue(xblock_info['published']) - self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) @@ -1179,7 +1185,6 @@ def validate_sequential_xblock_info(self, xblock_info, has_child_info=True): self.assertEqual(xblock_info['id'], 'i4x://MITx/999/sequential/Lesson_1') self.assertEqual(xblock_info['display_name'], 'Lesson 1') self.assertTrue(xblock_info['published']) - self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) @@ -1204,7 +1209,7 @@ def validate_vertical_xblock_info(self, xblock_info): self.validate_course_xblock_info(ancestors[2], has_child_info=False) # Finally, validate the entire response for consistency - self.validate_xblock_info_consistency(xblock_info, has_child_info=True, has_ancestor_info=True) + self.validate_xblock_info_consistency(xblock_info, has_child_info=True, has_ancestor_info=True, has_edited_by=True) def validate_component_xblock_info(self, xblock_info): """ @@ -1214,20 +1219,18 @@ def validate_component_xblock_info(self, xblock_info): self.assertEqual(xblock_info['id'], 'i4x://MITx/999/video/My_Video') self.assertEqual(xblock_info['display_name'], 'My Video') self.assertTrue(xblock_info['published']) - self.assertEqual(xblock_info['edited_by'], 'testuser') # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info) - def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, has_child_info=False): + def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, has_child_info=False, has_edited_by=False): """ Validate that the xblock info is internally consistent. """ self.assertIsNotNone(xblock_info['display_name']) self.assertIsNotNone(xblock_info['id']) self.assertIsNotNone(xblock_info['category']) - self.assertIsNotNone(xblock_info['published']) - self.assertEqual(xblock_info['edited_by'], 'testuser') + self.assertTrue(xblock_info['published']) if has_ancestor_info: self.assertIsNotNone(xblock_info.get('ancestor_info', None)) ancestors = xblock_info['ancestor_info']['ancestors'] @@ -1248,3 +1251,197 @@ def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, ) else: self.assertIsNone(xblock_info.get('child_info', None)) + if has_edited_by: + self.assertEqual(xblock_info['edited_by'], 'testuser') + else: + self.assertIsNone(xblock_info.get('edited_by', None)) + + +class TestXBlockPublishingInfo(ItemTest): + """ + Unit tests for XBlock's outline handling. + """ + FIRST_SUBSECTION_PATH = [0] + FIRST_UNIT_PATH = [0, 0] + SECOND_UNIT_PATH = [0, 1] + + def _create_child(self, parent, category, display_name, publish_item=False, staff_only=False): + """ + Creates a child xblock for the given parent. + """ + return ItemFactory.create( + parent_location=parent.location, category=category, display_name=display_name, + user_id=self.user.id, publish_item=publish_item, visible_to_staff_only=staff_only + ) + + def _get_child_xblock_info(self, xblock_info, index): + """ + Returns the child xblock info at the specified index. + """ + children = xblock_info['child_info']['children'] + self.assertTrue(len(children) > index) + return children[index] + + def _get_xblock_info(self, location): + """ + Returns the xblock info for the specified location. + """ + return create_xblock_info( + modulestore().get_item(location), + include_child_info=True, + include_children_predicate=ALWAYS, + ) + + def _set_release_date(self, location, start): + """ + Sets the release date for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.start = start + self.store.update_item(xblock, self.user.id) + + def _set_staff_only(self, location, staff_only): + """ + Sets staff only for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.visible_to_staff_only = staff_only + self.store.update_item(xblock, self.user.id) + + def _set_display_name(self, location, display_name): + """ + Sets the display name for the specified xblock. + """ + xblock = modulestore().get_item(location) + xblock.display_name = display_name + self.store.update_item(xblock, self.user.id) + + def _verify_visibility_state(self, xblock_info, expected_state, path=None): + """ + Verify the publish state of an item in the xblock_info. If no path is provided + then the root item will be verified. + """ + if path: + direct_child_xblock_info = self._get_child_xblock_info(xblock_info, path[0]) + remaining_path = path[1:] if len(path) > 1 else None + self._verify_visibility_state(direct_child_xblock_info, expected_state, remaining_path) + else: + self.assertEqual(xblock_info['visibility_state'], expected_state) + + def test_empty_chapter(self): + empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter") + xblock_info = self._get_xblock_info(empty_chapter.location) + self.assertEqual(xblock_info['visibility_state'], VisibilityState.unscheduled) + + def test_empty_sequential(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + self._create_child(chapter, 'sequential', "Empty Sequential") + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.unscheduled) + self._verify_visibility_state(xblock_info, VisibilityState.unscheduled, path=self.FIRST_SUBSECTION_PATH) + + def test_published_unit(self): + """ + Tests the visibility state of a published unit with release date in the future. + """ + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.ready) + self._verify_visibility_state(xblock_info, VisibilityState.ready, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.ready, path=self.FIRST_UNIT_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) + + def test_released_unit(self): + """ + Tests the visibility state of a published unit with release date in the past. + """ + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.live) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_UNIT_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) + + def test_unpublished_changes(self): + """ + Tests the visibility state of a published unit with draft (unpublished) changes. + """ + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + unit = self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) + # Setting the display name creates a draft version of unit. + self._set_display_name(unit.location, 'Updated Unit') + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) + self._verify_visibility_state(xblock_info, VisibilityState.needs_attention, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.needs_attention, path=self.FIRST_UNIT_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) + + def test_partially_released_section(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + released_sequential = self._create_child(chapter, 'sequential', "Released Sequential") + self._create_child(released_sequential, 'vertical', "Released Unit", publish_item=True) + self._create_child(released_sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) + published_sequential = self._create_child(chapter, 'sequential', "Published Sequential") + self._create_child(published_sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(published_sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(published_sequential.location, datetime.now(UTC) + timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + + # Verify the state of the released sequential + self._verify_visibility_state(xblock_info, VisibilityState.live, path=[0]) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=[0, 0]) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[0, 1]) + + # Verify the state of the published sequential + self._verify_visibility_state(xblock_info, VisibilityState.ready, path=[1]) + self._verify_visibility_state(xblock_info, VisibilityState.ready, path=[1, 0]) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[1, 1]) + + # Finally verify the state of the chapter + self._verify_visibility_state(xblock_info, VisibilityState.ready) + + def test_staff_only(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + unit = self._create_child(sequential, 'vertical', "Published Unit") + self._set_staff_only(unit.location, True) + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH) + + def test_unscheduled_section_with_live_subsection(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(sequential.location, datetime.now(UTC) - timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_UNIT_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) + + def test_unreleased_section_with_live_subsection(self): + chapter = self._create_child(self.course, 'chapter', "Test Chapter") + sequential = self._create_child(chapter, 'sequential', "Test Sequential") + self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) + self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) + self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) + self._set_release_date(sequential.location, datetime.now(UTC) - timedelta(days=1)) + xblock_info = self._get_xblock_info(chapter.location) + self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_SUBSECTION_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_UNIT_PATH) + self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js index 8cee477c0e14..1be5ef7345aa 100644 --- a/cms/static/js/models/xblock_info.js +++ b/cms/static/js/models/xblock_info.js @@ -24,21 +24,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * An optional object with information about each of the ancestors. */ "ancestor_info": null, - /** - * True iff: - * 1) Edits have been made to the xblock and no published version exists. - * 2) Edits have been made to the xblock since the last published version. - */ - "has_changes": null, - /** - * True iff a published version of the xblock exists. - */ - "published": null, - /** - * If true, only course staff can see the xblock regardless of publish status or - * release date status. - */ - "visible_to_staff_only": null, /** * Date of the last edit to this xblock or any of its descendants. */ @@ -47,6 +32,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * User who last edited the xblock or any of its descendants. */ "edited_by":null, + /** + * True iff a published version of the xblock exists. + */ + "published": null, /** * Date of the last publish of this xblock, or null if never published. */ @@ -55,6 +44,17 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu * User who last published the xblock, or null if never published. */ "published_by": null, + /** + * True if the xblock has changes. + * Note: this is not always provided as a performance optimization. It is only provided for + * verticals functioning as units. + */ + "has_changes": null, + /** + * Represents the possible publish states for an xblock. See the documentation + * for XBlockVisibility to see a comprehensive enumeration of the states. + */ + "visibility_state": null, /** * True iff the release date of the xblock is in the past. */ diff --git a/cms/static/js/spec/views/pages/container_subviews_spec.js b/cms/static/js/spec/views/pages/container_subviews_spec.js index 6aaaedc6ef1a..a4c5ef176227 100644 --- a/cms/static/js/spec/views/pages/container_subviews_spec.js +++ b/cms/static/js/spec/views/pages/container_subviews_spec.js @@ -1,7 +1,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers", "js/views/feedback_prompt", "js/views/pages/container", "js/views/pages/container_subviews", - "js/models/xblock_info"], - function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, ContainerSubviews, XBlockInfo) { + "js/models/xblock_info", "js/views/utils/xblock_utils"], + function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, ContainerSubviews, + XBlockInfo, XBlockUtils) { + var VisibilityState = XBlockUtils.VisibilityState; describe("Container Subviews", function() { var model, containerPage, requests, createContainerPage, renderContainerPage, @@ -25,9 +27,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin category: 'vertical', published: false, has_changes: false, + visibility_state: VisibilityState.unscheduled, edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe", published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako", - visible_to_staff_only: false, currently_visible_to_students: false }; @@ -79,31 +81,31 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin var viewPublishedCss = '.button-view', previewCss = '.button-preview'; - it('renders correctly for private unit', function () { + it('renders correctly for unscheduled unit', function () { renderContainerPage(this, mockContainerXBlockHtml); expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); }); - it('updates when published attribute changes', function () { + it('updates when publish state changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true}); + fetch({published: true}); expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss); - fetch({"published": false}); + fetch({published: false}); expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss); }); it('updates when has_changes attribute changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"has_changes": true}); + fetch({has_changes: true}); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); - fetch({"published": true, "has_changes": false}); + fetch({published: true, has_changes: false}); expect(containerPage.$(previewCss)).toHaveClass(disabledCss); // If published is false, preview is always enabled. - fetch({"published": false, "has_changes": false}); + fetch({published: false, has_changes: false}); expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss); }); }); @@ -111,23 +113,26 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Publisher", function () { var headerCss = '.pub-status', bitPublishingCss = "div.bit-publishing", - publishedBit = "is-published", - draftBit = "is-draft", - staffOnlyBit = "is-staff-only", + liveClass = "is-live", + readyClass = "is-ready", + staffOnlyClass = "is-staff-only", + scheduledClass = "is-scheduled", + unscheduledClass = "", + hasWarningsClass = 'has-warnings', publishButtonCss = ".action-publish", discardChangesButtonCss = ".action-discard", lastDraftCss = ".wrapper-last-draft", releaseDateTitleCss = ".wrapper-release .title", releaseDateContentCss = ".wrapper-release .copy", - promptSpies, sendDiscardChangesToServer; + promptSpies, sendDiscardChangesToServer, verifyPublishingBitUnscheduled; sendDiscardChangesToServer = function() { // Helper function to do the discard operation, up until the server response. containerPage.render(); respondWithHtml(mockContainerXBlockHtml); - fetch({"published": true, "has_changes": true}); + fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention}); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); - expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass); // Click discard changes containerPage.$(discardChangesButtonCss).click(); @@ -140,6 +145,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin ); }; + verifyPublishingBitUnscheduled = function() { + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(unscheduledClass); + }; + beforeEach(function() { promptSpies = spyOnConstructor(Prompt, "Warning", ["show", "hide"]); promptSpies.show.andReturn(this.promptSpies); @@ -150,38 +164,64 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin expect(containerPage.$(headerCss).text()).toContain('Draft (Never published)'); expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(scheduledClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass); }; renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "has_changes": false}); + fetch({published: false, has_changes: false, visibility_state: VisibilityState.needsAttention}); verifyPrivateState(); - fetch({"published": false, "has_changes": true}); + fetch({published: false, has_changes: true, visibility_state: VisibilityState.needsAttention}); verifyPrivateState(); }); - it('renders correctly with public content', function () { + it('renders correctly with published content', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "has_changes": false}); + fetch({ + published: true, has_changes: false, visibility_state: VisibilityState.ready, + release_date: "Jul 02, 2030 at 14:20 UTC" + }); expect(containerPage.$(headerCss).text()).toContain('Published'); expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass); - fetch({"published": true, "has_changes": true}); + fetch({ + published: true, has_changes: true, visibility_state: VisibilityState.needsAttention, + release_date: "Jul 02, 2030 at 14:20 UTC" + }); expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)'); expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss); expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss); - expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass); + + fetch({published: true, has_changes: false, visibility_state: VisibilityState.live, + release_date: "Jul 02, 1990 at 14:20 UTC" + }); + expect(containerPage.$(headerCss).text()).toContain('Published and Live'); + expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss); + expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); + expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass); + + fetch({published: true, has_changes: false, visibility_state: VisibilityState.unscheduled, + release_date: null + }); + expect(containerPage.$(headerCss).text()).toContain('Published'); + expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss); + expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss); + verifyPublishingBitUnscheduled(); }); it('can publish private content', function () { var notificationSpy = edit_helpers.createNotificationSpy(); renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "has_changes": false}); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass); // Click publish containerPage.$(publishButtonCss).click(); @@ -197,18 +237,19 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin create_sinon.expectJsonRequest(requests, "GET", "/xblock/locator-container"); // Response to fetch - respondWithJson({"id": "locator-container", "published": true, "has_changes": false}); + respondWithJson(createXBlockInfo({ + published: true, has_changes: false, visibility_state: VisibilityState.ready + })); // Verify updates displayed - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass); // Verify that the "published" value has been cleared out of the model. expect(containerPage.model.get("publish")).toBeNull(); }); - it('can does not fetch if publish fails', function () { + it('does not refresh if publish fails', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false}); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + verifyPublishingBitUnscheduled(); // Click publish containerPage.$(publishButtonCss).click(); @@ -219,8 +260,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin expect(requests.length).toEqual(numRequests); - // Verify still in draft state. - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); + // Verify still in draft (unscheduled) state. + verifyPublishingBitUnscheduled(); // Verify that the "published" value has been cleared out of the model. expect(containerPage.model.get("publish")).toBeNull(); }); @@ -264,10 +305,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('does not discard changes on cancel', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "has_changes": true}); + fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention}); var numRequests = requests.length; // Click discard changes + expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled'); containerPage.$(discardChangesButtonCss).click(); // Click cancel to confirmation. @@ -280,14 +322,14 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders the last published date and user when there are no changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"}); + fetch({published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako"}); expect(containerPage.$(lastDraftCss).text()). toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); }); it('renders the last saved date and user when there are changes', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"has_changes": true, "edited_on": "Jul 02, 2014 at 14:20 UTC", "edited_by": "joe"}); + fetch({has_changes: true, edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe"}); expect(containerPage.$(lastDraftCss).text()). toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe"); }); @@ -295,8 +337,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("Release Date", function() { it('renders correctly when unreleased', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": false, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"'}); + fetch({ + visibility_state: VisibilityState.ready, released_to_students: false, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Scheduled:"); expect(containerPage.$(releaseDateContentCss).text()). toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"'); @@ -304,8 +348,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders correctly when released', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": true, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' }); + fetch({ + visibility_state: VisibilityState.live, released_to_students: true, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Released:"); expect(containerPage.$(releaseDateContentCss).text()). toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"'); @@ -313,17 +359,20 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it('renders correctly when the release date is not set', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": true, "released_to_students": false, - "release_date": null, "release_date_from": null }); + fetch({ + visibility_state: VisibilityState.unscheduled, "released_to_students": false, + release_date: null, release_date_from: null + }); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:"); expect(containerPage.$(releaseDateContentCss).text()).toContain("Unscheduled"); }); it('renders correctly when the unit is not published', function () { renderContainerPage(this, mockContainerXBlockHtml); - fetch({"published": false, "released_to_students": true, - "release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' }); - // Force a render because none of the fetched fields will trigger a render + fetch({ + visibility_state: VisibilityState.needsAttention, released_to_students: true, + release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"' + }); containerPage.xblockPublisher.render(); expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:"); expect(containerPage.$(releaseDateContentCss).text()). @@ -356,7 +405,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container'); create_sinon.respondWithJson(requests, createXBlockInfo({ published: containerPage.model.get('published'), - visible_to_staff_only: isStaffOnly + visibility_state: isStaffOnly ? VisibilityState.staffOnly : VisibilityState.live, + release_date: "Jul 02, 2000 at 14:20 UTC" })); }; @@ -364,11 +414,12 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin if (isStaffOnly) { expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check'); expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff Only'); - expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyBit); + expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass); + expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass); } else { expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check-empty'); expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff and Students'); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyBit); + expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass); } }; @@ -379,34 +430,27 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin it("can be set to staff only", function() { renderContainerPage(this, mockContainerXBlockHtml); - containerPage.$('.action-staff-lock').click(); requestStaffOnly(true); verifyStaffOnly(true); }); it("can remove staff only setting", function() { promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - requestStaffOnly(true); - requestStaffOnly(false); - verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit); - }); - - it("can remove staff only setting from published unit", function() { - promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml, { published: true }); - requestStaffOnly(true); + renderContainerPage(this, mockContainerXBlockHtml, { + visibility_state: VisibilityState.staffOnly, + release_date: "Jul 02, 2000 at 14:20 UTC" + }); requestStaffOnly(false); verifyStaffOnly(false); - expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit); }); it("does not refresh if removing staff only is canceled", function() { var requestCount; promptSpy = edit_helpers.createPromptSpy(); - renderContainerPage(this, mockContainerXBlockHtml); - requestStaffOnly(true); + renderContainerPage(this, mockContainerXBlockHtml, { + visibility_state: VisibilityState.staffOnly, + release_date: "Jul 02, 2000 at 14:20 UTC" + }); requestCount = requests.length; containerPage.$('.action-staff-lock').click(); edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel @@ -429,25 +473,26 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin describe("PublishHistory", function () { var lastPublishCss = ".wrapper-last-publish"; + it('renders never published when the block is unpublished', function () { + renderContainerPage(this, mockContainerXBlockHtml, { + published: false, published_on: null, published_by: null + }); + expect(containerPage.$(lastPublishCss).text()).toContain("Never published"); + }); + it('renders the last published date and user when the block is published', function() { renderContainerPage(this, mockContainerXBlockHtml); fetch({ - "published": true, "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" + published: true, published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako" }); expect(containerPage.$(lastPublishCss).text()). toContain("Last published Jul 01, 2014 at 12:45 UTC by amako"); }); - it('renders never published when the block is unpublished', function () { - renderContainerPage(this, mockContainerXBlockHtml); - fetch({ "published": false }); - expect(containerPage.$(lastPublishCss).text()).toContain("Never published"); - }); - it('renders correctly when the block is published without publish info', function () { renderContainerPage(this, mockContainerXBlockHtml); fetch({ - "published": true, "published_on": null, "published_by": null + published: true, published_on: null, published_by: null }); expect(containerPage.$(lastPublishCss).text()).toContain("Previously published"); }); diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 727b183dc16d..98d1ac611078 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -4,7 +4,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" describe("CourseOutlinePage", function() { var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, - getHeaderElement, expandAndVerifyState, collapseAndVerifyState, + getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'); @@ -64,21 +64,31 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }; }; - getHeaderElement = function(selector) { - var element = outlinePage.$(selector); - return element.find('> .wrapper-xblock-header'); + getItemsOfType = function(type) { + return outlinePage.$('.outline-' + type); }; - expandAndVerifyState = function(selector) { - var element = outlinePage.$(selector); - getHeaderElement(selector).find('.ui-toggle-expansion').click(); - expect(element).not.toHaveClass('collapsed'); + getItemHeaders = function(type) { + return getItemsOfType(type).find('> .' + type + '-header'); }; - collapseAndVerifyState = function(selector) { - var element = outlinePage.$(selector); - getHeaderElement(selector).find('.ui-toggle-expansion').click(); - expect(element).toHaveClass('collapsed'); + verifyItemsExpanded = function(type, isExpanded) { + var element = getItemsOfType(type); + if (isExpanded) { + expect(element).not.toHaveClass('is-collapsed'); + } else { + expect(element).toHaveClass('is-collapsed'); + } + }; + + expandItemsAndVerifyState = function(type) { + getItemHeaders(type).find('.ui-toggle-expansion').click(); + verifyItemsExpanded(type, true); + }; + + collapseItemsAndVerifyState = function(type) { + getItemHeaders(type).find('.ui-toggle-expansion').click(); + verifyItemsExpanded(type, false); }; createCourseOutlinePage = function(test, courseJSON, createOnly) { @@ -108,12 +118,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" category: 'vertical', studio_url: '/container/mock-unit', is_container: true, - has_changes: true, - published: false, + has_changes: false, + published: true, + visibility_state: 'unscheduled', edited_on: 'Jul 02, 2014 at 20:56 UTC', edited_by: 'MockUser' - } - ]) + }]) ]) ]); mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []); @@ -129,9 +139,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" describe('Initial display', function() { it('can render itself', function() { createCourseOutlinePage(this, mockCourseJSON); - expect(outlinePage.$('.sortable-course-list')).toExist(); - expect(outlinePage.$('.sortable-section-list')).toExist(); - expect(outlinePage.$('.sortable-subsection-list')).toExist(); + expect(outlinePage.$('.list-sections')).toExist(); + expect(outlinePage.$('.list-subsections')).toExist(); + expect(outlinePage.$('.list-units')).toExist(); }); it('shows a loading indicator', function() { @@ -142,18 +152,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it('shows subsections initially collapsed', function() { - var subsectionElement; createCourseOutlinePage(this, mockCourseJSON); - subsectionElement = outlinePage.$('.outline-item-subsection'); - expect(subsectionElement).toHaveClass('collapsed'); - expect(outlinePage.$('.outline-item-unit')).not.toExist(); + verifyItemsExpanded('subsection', false); + expect(getItemsOfType('unit')).not.toExist(); }); }); describe("Button bar", function() { it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); - outlinePage.$('.nav-actions .add-button').click(); + outlinePage.$('.nav-actions .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -166,13 +174,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('can add a second section', function() { var sectionElements; createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.nav-actions .add-button').click(); + outlinePage.$('.nav-actions .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -186,7 +194,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2'); create_sinon.respondWithJson(requests, createMockSectionJSON('mock-section-2', 'Mock Section 2', [])); - sectionElements = outlinePage.$('.sortable-course-list .outline-item-section'); + sectionElements = getItemsOfType('section'); expect(sectionElements.length).toBe(2); expect($(sectionElements[0]).data('locator')).toEqual('mock-section'); expect($(sectionElements[1]).data('locator')).toEqual('mock-section-2'); @@ -194,10 +202,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can expand and collapse all sections', function() { createCourseOutlinePage(this, mockCourseJSON, false); - outlinePage.$('.nav-actions .toggle-button-expand-collapse').click(); - expect(outlinePage.$('.outline-item-section')).toHaveClass('collapsed'); - outlinePage.$('.nav-actions .toggle-button-expand-collapse').click(); - expect(outlinePage.$('.outline-item-section')).not.toHaveClass('collapsed'); + verifyItemsExpanded('section', true); + outlinePage.$('.nav-actions .button-toggle-expand-collapse .collapse-all').click(); + verifyItemsExpanded('section', false); + outlinePage.$('.nav-actions .button-toggle-expand-collapse .expand-all').click(); + verifyItemsExpanded('section', true); }); }); @@ -205,12 +214,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('shows an empty course message initially', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); it('can add a section', function() { createCourseOutlinePage(this, mockEmptyCourseJSON); - $('.no-content .add-button').click(); + $('.no-content .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -223,13 +232,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON); expect(outlinePage.$('.no-content')).not.toExist(); - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('remains empty if an add fails', function() { var requestCount; createCourseOutlinePage(this, mockEmptyCourseJSON); - $('.no-content .add-button').click(); + $('.no-content .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'chapter', 'display_name': 'Section', @@ -239,7 +248,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); }); @@ -247,7 +256,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var getDisplayNameWrapper; getDisplayNameWrapper = function() { - return getHeaderElement('.outline-item-section').find('.wrapper-xblock-field').first(); + return getItemHeaders('section').find('.wrapper-xblock-field'); }; it('can be deleted', function() { @@ -256,7 +265,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createMockSectionJSON('mock-section', 'Mock Section', []), createMockSectionJSON('mock-section-2', 'Mock Section 2', []) ])); - outlinePage.$('.outline-item-section .delete-button').first().click(); + getItemHeaders('section').find('.delete-button').first().click(); view_helpers.confirmPrompt(promptSpy); requestCount = requests.length; create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); @@ -269,32 +278,32 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be deleted if it is the only section', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.outline-item-section .delete-button').click(); + getItemHeaders('section').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); create_sinon.respondWithJson(requests, {}); create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course'); create_sinon.respondWithJson(requests, mockEmptyCourseJSON); expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden'); - expect(outlinePage.$('.no-content .add-button')).toExist(); + expect(outlinePage.$('.no-content .button-new')).toExist(); }); it('remains visible if its deletion fails', function() { var promptSpy = view_helpers.createPromptSpy(), requestCount; createCourseOutlinePage(this, mockSingleSectionCourseJSON); - outlinePage.$('.outline-item-section .delete-button').click(); + getItemHeaders('section').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section'); requestCount = requests.length; create_sinon.respondWithError(requests); expect(requests.length).toBe(requestCount); // No additional requests should be made - expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section'); + expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section'); }); it('can add a subsection', function() { createCourseOutlinePage(this, mockCourseJSON); - outlinePage.$('.outline-item-section > .add-xblock-component .add-button').click(); + getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'sequential', 'display_name': 'Subsection', @@ -329,9 +338,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be expanded and collapsed', function() { createCourseOutlinePage(this, mockCourseJSON); - collapseAndVerifyState('.outline-item-section'); - expandAndVerifyState('.outline-item-section'); - collapseAndVerifyState('.outline-item-section'); + collapseItemsAndVerifyState('section'); + expandItemsAndVerifyState('section'); + collapseItemsAndVerifyState('section'); }); }); @@ -339,13 +348,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var getDisplayNameWrapper; getDisplayNameWrapper = function() { - return getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first(); + return getItemHeaders('subsection').find('.wrapper-xblock-field'); }; it('can be deleted', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); - getHeaderElement('.outline-item-subsection').find('.delete-button').click(); + getItemHeaders('subsection').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection'); create_sinon.respondWithJson(requests, {}); @@ -358,7 +367,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" var redirectSpy; createCourseOutlinePage(this, mockCourseJSON); redirectSpy = spyOn(ViewUtils, 'redirect'); - outlinePage.$('.outline-item-subsection > .add-xblock-component .add-button').click(); + getItemsOfType('subsection').find('> .outline-content > .add-unit .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { 'category': 'vertical', 'display_name': 'Unit', @@ -387,20 +396,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" createMockSubsectionJSON('mock-subsection', updatedDisplayName, []) ])); // Find the display name again in the refreshed DOM and verify it - displayNameWrapper = getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first(); + displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field'); view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0]; expect(subsectionModel.get('display_name')).toBe(updatedDisplayName); }); it('can be expanded and collapsed', function() { - var subsectionElement; createCourseOutlinePage(this, mockCourseJSON); - subsectionElement = outlinePage.$('.outline-item-subsection'); - expect(subsectionElement).toHaveClass('collapsed'); - expandAndVerifyState('.outline-item-subsection'); - collapseAndVerifyState('.outline-item-subsection'); - expandAndVerifyState('.outline-item-subsection'); + verifyItemsExpanded('subsection', false); + expandItemsAndVerifyState('subsection'); + collapseItemsAndVerifyState('subsection'); + expandItemsAndVerifyState('subsection'); }); }); @@ -409,8 +416,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can be deleted', function() { var promptSpy = view_helpers.createPromptSpy(); createCourseOutlinePage(this, mockCourseJSON); - expandAndVerifyState('.outline-item-subsection'); - getHeaderElement('.outline-item-unit').find('.delete-button').click(); + expandItemsAndVerifyState('subsection'); + getItemHeaders('unit').find('.delete-button').click(); view_helpers.confirmPrompt(promptSpy); create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit'); create_sinon.respondWithJson(requests, {}); @@ -420,11 +427,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" }); it('has a link to the unit page', function() { - var anchor; + var unitAnchor; createCourseOutlinePage(this, mockCourseJSON); - expandAndVerifyState('.outline-item-subsection'); - anchor = outlinePage.$('.outline-item-unit .xblock-title a'); - expect(anchor.attr('href')).toBe('/container/mock-unit'); + expandItemsAndVerifyState('subsection'); + unitAnchor = getItemsOfType('unit').find('.unit-title a'); + expect(unitAnchor.attr('href')).toBe('/container/mock-unit'); }); }); }); diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js index 5396ce896c9d..117964751aa0 100644 --- a/cms/static/js/spec/views/unit_outline_spec.js +++ b/cms/static/js/spec/views/unit_outline_spec.js @@ -25,12 +25,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" category: 'vertical', display_name: displayName, studio_url: '/container/mock-unit', + visibility_state: 'unscheduled', ancestor_info: { ancestors: [{ id: 'mock-subsection', category: 'sequential', display_name: 'Mock Subsection', studio_url: '/course/mock-course?show=mock-subsection', + visibility_state: 'unscheduled', child_info: { category: 'vertical', display_name: 'Unit', @@ -38,24 +40,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" id: 'mock-unit', category: 'vertical', display_name: displayName, - studio_url: '/container/mock-unit' + studio_url: '/container/mock-unit', + visibility_state: 'unscheduled' }, { id: 'mock-unit-2', category: 'vertical', display_name: 'Mock Unit 2', - studio_url: '/container/mock-unit-2' + studio_url: '/container/mock-unit-2', + visibility_state: 'unscheduled' }] } }, { id: 'mock-section', category: 'chapter', display_name: 'Section', - studio_url: '/course/slashes:mock-course?show=mock-section' + studio_url: '/course/slashes:mock-course?show=mock-section', + visibility_state: 'unscheduled' }, { id: 'mock-course', category: 'course', display_name: 'Mock Course', - studio_url: '/course/mock-course' + studio_url: '/course/mock-course', + visibility_state: 'unscheduled' }] }, metadata: { @@ -77,16 +83,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" it('can render itself', function() { createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); - expect(unitOutlineView.$('.sortable-course-list')).toExist(); - expect(unitOutlineView.$('.sortable-section-list')).toExist(); - expect(unitOutlineView.$('.sortable-subsection-list')).toExist(); + expect(unitOutlineView.$('.list-sections')).toExist(); + expect(unitOutlineView.$('.list-subsections')).toExist(); + expect(unitOutlineView.$('.list-units')).toExist(); }); it('can add a unit', function() { var redirectSpy; createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); redirectSpy = spyOn(ViewUtils, 'redirect'); - unitOutlineView.$('.outline-item-subsection > .add-xblock-component .add-button').click(); + unitOutlineView.$('.outline-subsection > .outline-content > .add-unit .button-new').click(); create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', { category: 'vertical', display_name: 'Unit', @@ -106,8 +112,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" create_sinon.expectJsonRequest(requests, 'GET', '/xblock/mock-unit'); create_sinon.respondWithJson(requests, createMockXBlockInfo(updatedDisplayName)); - unitHeader = unitOutlineView.$('.outline-item-unit .wrapper-xblock-header'); - expect(unitHeader.find('.xblock-title').first().text().trim()).toBe(updatedDisplayName); + expect(unitOutlineView.$('.outline-unit .unit-title').first().text().trim()).toBe(updatedDisplayName); }); }); }); diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js index bf0d456bbe05..32ffd0171b4a 100644 --- a/cms/static/js/views/baseview.js +++ b/cms/static/js/views/baseview.js @@ -16,6 +16,13 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b "click .ui-toggle-expansion": "toggleExpandCollapse" }, + options: { + // UX is moving towards using 'is-collapsed' in preference over 'collapsed', + // but use the old scheme as the default so that existing code doesn't need + // to be rewritten. + collapsedClass: 'collapsed' + }, + //override the constructor function constructor: function(options) { _.bindAll(this, 'beforeRender', 'render', 'afterRender'); @@ -48,7 +55,7 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b // this element, e.g. clicking on the element of a child view container in a parent. event.stopPropagation(); event.preventDefault(); - ViewUtils.toggleExpandCollapse(target); + ViewUtils.toggleExpandCollapse(target, this.options.collapsedClass); }, /** diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 72f61aae8dc3..88f787055d85 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -46,7 +46,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ var expandedLocators = []; this.$('.outline-item.is-collapsible').each(function(index, rawElement) { var element = $(rawElement); - if (!element.hasClass('collapsed')) { + if (!element.hasClass('is-collapsed')) { expandedLocators.push(element.data('locator')); } }); diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index cb2c33bc57f7..257b76fe15dd 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -12,6 +12,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views var XBlockContainerPage = BasePage.extend({ // takes XBlockInfo as a model + options: { + collapsedClass: 'is-collapsed' + }, + view: 'container_preview', initialize: function(options) { diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 7bf27078a8d1..21c0ccd76a95 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -1,13 +1,14 @@ /** * Subviews (usually small side panels) for XBlockContainerPage. */ -define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/view_utils"], - function ($, _, gettext, BaseView, ViewUtils) { - - var disabledCss = "is-disabled"; +define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/view_utils", + "js/views/utils/xblock_utils"], + function ($, _, gettext, BaseView, ViewUtils, XBlockViewUtils) { + var VisibilityState = XBlockViewUtils.VisibilityState, + disabledCss = "is-disabled"; /** - * A view that calls render when "has_changes" or "published" values in XBlockInfo have changed + * A view that refreshes the view when certain values in the XBlockInfo have changed * after a server sync operation. */ var ContainerStateListenerView = BaseView.extend({ @@ -99,7 +100,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ onSync: function(model) { if (ViewUtils.hasChangedAttributes(model, [ - 'has_changes', 'published', 'edited_on', 'edited_by', 'visible_to_staff_only' + 'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state' ])) { this.render(); } @@ -107,16 +108,16 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ render: function () { this.$el.html(this.template({ + visibilityState: this.model.get('visibility_state'), + visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(this.model.get('visibility_state')), hasChanges: this.model.get('has_changes'), - published: this.model.get('published'), editedOn: this.model.get('edited_on'), editedBy: this.model.get('edited_by'), + published: this.model.get('published'), publishedOn: this.model.get('published_on'), publishedBy: this.model.get('published_by'), - releasedToStudents: this.model.get('released_to_students'), releaseDate: this.model.get('release_date'), - releaseDateFrom: this.model.get('release_date_from'), - visibleToStaffOnly: this.model.get('visible_to_staff_only') + releaseDateFrom: this.model.get('release_date_from') })); return this; @@ -138,7 +139,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ }, discardChanges: function (e) { - var xblockInfo = this.model, that=this, renderPage = this.renderPage; + var xblockInfo = this.model, renderPage = this.renderPage; if (e && e.preventDefault) { e.preventDefault(); } @@ -164,7 +165,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ if (e && e.preventDefault) { e.preventDefault(); } - enableStaffLock = !xblockInfo.get('visible_to_staff_only'); + enableStaffLock = xblockInfo.get('visibility_state') !== VisibilityState.staffOnly; revertCheckBox = function() { self.checkStaffLock(!enableStaffLock); diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js index 95367b0626ed..58c0d263d53d 100644 --- a/cms/static/js/views/pages/course_outline.js +++ b/cms/static/js/views/pages/course_outline.js @@ -8,14 +8,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views // takes XBlockInfo as a model events: { - "click .toggle-button-expand-collapse": "toggleExpandCollapse" + "click .button-toggle-expand-collapse": "toggleExpandCollapse" + }, + + options: { + collapsedClass: 'is-collapsed' }, initialize: function() { var self = this; this.initialState = this.options.initialState; BasePage.prototype.initialize.call(this); - this.$('.add-button').click(function(event) { + this.$('.button-new').click(function(event) { self.outlineView.handleAddEvent(event); }); this.model.on('change', this.setCollapseExpandVisibility, this); @@ -23,19 +27,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views setCollapseExpandVisibility: function() { var has_content = this.hasContent(), - collapseExpandButton = $('.toggle-button-expand-collapse'); + collapseExpandButton = $('.button-toggle-expand-collapse'); if (has_content) { - collapseExpandButton.show(); + collapseExpandButton.removeClass('is-hidden'); } else { - collapseExpandButton.hide(); + collapseExpandButton.addClass('is-hidden'); } }, renderPage: function() { - var locatorToShow; this.setCollapseExpandVisibility(); this.outlineView = new CourseOutlineView({ - el: this.$('.course-outline'), + el: this.$('.outline'), model: this.model, isRoot: true, initialState: this.initialState @@ -50,19 +53,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views }, toggleExpandCollapse: function(event) { - var toggleButton = this.$('.toggle-button-expand-collapse'), + var toggleButton = this.$('.button-toggle-expand-collapse'), collapse = toggleButton.hasClass('collapse-all'); event.preventDefault(); toggleButton.toggleClass('collapse-all expand-all'); - this.$('.course-outline > ol > li').each(function(index, domElement) { - var element = $(domElement), - expandCollapseElement = element.find('.expand-collapse').first(); + this.$('.list-sections > li').each(function(index, domElement) { + var element = $(domElement); if (collapse) { - expandCollapseElement.removeClass('expand').addClass('collapse'); - element.addClass('collapsed'); + element.addClass('is-collapsed'); } else { - expandCollapseElement.addClass('expand').removeClass('collapse'); - element.removeClass('collapsed'); + element.removeClass('is-collapsed'); } }); } diff --git a/cms/static/js/views/unit_outline.js b/cms/static/js/views/unit_outline.js index 49366dadfe83..cea6b91b6825 100644 --- a/cms/static/js/views/unit_outline.js +++ b/cms/static/js/views/unit_outline.js @@ -23,7 +23,7 @@ define(['js/views/xblock_outline'], previousAncestor = null; if (this.model.get('ancestor_info')) { ancestors = this.model.get('ancestor_info').ancestors; - listElement = this.$('.sortable-list'); + listElement = this.getListElement(); // Note: the ancestors are processed in reverse order because the tree wants to // start at the root, but the ancestors are ordered by closeness to the unit, // i.e. subsection and then section. @@ -33,7 +33,7 @@ define(['js/views/xblock_outline'], ancestorView.render(); listElement.append(ancestorView.$el); previousAncestor = ancestor; - listElement = ancestorView.$('.sortable-list'); + listElement = ancestorView.getListElement(); } } return ancestorView; diff --git a/cms/static/js/views/utils/view_utils.js b/cms/static/js/views/utils/view_utils.js index f4f44126a82e..98eab24d961c 100644 --- a/cms/static/js/views/utils/view_utils.js +++ b/cms/static/js/views/utils/view_utils.js @@ -10,9 +10,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js /** * Toggles the expanded state of the current element. */ - toggleExpandCollapse = function(target) { + toggleExpandCollapse = function(target, collapsedClass) { + // Support the old 'collapsed' option until fully switched over to is-collapsed + if (!collapsedClass) { + collapsedClass = 'collapsed'; + } target.closest('.expand-collapse').toggleClass('expand collapse'); - target.closest('.is-collapsible, .window').toggleClass('collapsed'); + target.closest('.is-collapsible, .window').toggleClass(collapsedClass); target.closest('.is-collapsible').children('article').slideToggle(); }; diff --git a/cms/static/js/views/utils/xblock_utils.js b/cms/static/js/views/utils/xblock_utils.js index bb16af32a699..da6674c492fb 100644 --- a/cms/static/js/views/utils/xblock_utils.js +++ b/cms/static/js/views/utils/xblock_utils.js @@ -3,7 +3,36 @@ */ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/utils/module"], function($, _, gettext, ViewUtils, ModuleUtils) { - var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField; + var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState, + getXBlockVisibilityClass, getXBlockListTypeClass; + + /** + * Represents the possible visibility states for an xblock: + * + * live - the block and all of its descendants are live to students (excluding staff only) + * Note: Live means both published and released. + * + * ready - the block is ready to go live and all of its descendants are live or ready (excluding staff only) + * Note: content is ready when it is published and scheduled with a release date in the future. + * + * unscheduled - the block and all of its descendants have no release date (excluding staff only) + * Note: it is valid for items to be published with no release date in which case they are unscheduled. + * + * needsAttention - the block or its descendants need attention + * i.e. there is some content that is not fully live, ready, unscheduled or staff only. + * For example: one subsection has draft content, or there's both unreleased and released content + * in one section. + * + * staffOnly - all of the block's content is to be shown to staff only + * Note: staff only items do not affect their parent's state. + */ + VisibilityState = { + live: 'live', + ready: 'ready', + unscheduled: 'unscheduled', + needsAttention: 'needs_attention', + staffOnly: 'staff_only' + }; /** * Adds an xblock based upon the data attributes of the specified add button. A promise @@ -83,9 +112,43 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util }); }; + /** + * Returns the CSS class to represent the specified xblock visibility state. + */ + getXBlockVisibilityClass = function(visibilityState) { + if (visibilityState === VisibilityState.staffOnly) { + return 'is-staff-only'; + } + if (visibilityState === VisibilityState.live) { + return 'is-live'; + } + if (visibilityState === VisibilityState.ready) { + return 'is-ready'; + } + if (visibilityState === VisibilityState.needsAttention) { + return 'has-warnings'; + } + return ''; + }; + + getXBlockListTypeClass = function (xblockType) { + var listType = 'list-unknown'; + if (xblockType === 'course') { + listType = 'list-sections'; + } else if (xblockType === 'section') { + listType = 'list-subsections'; + } else if (xblockType === 'subsection') { + listType = 'list-units'; + } + return listType; + }; + return { + 'VisibilityState': VisibilityState, 'addXBlock': addXBlock, 'deleteXBlock': deleteXBlock, - 'updateXBlockField': updateXBlockField + 'updateXBlockField': updateXBlockField, + 'getXBlockVisibilityClass': getXBlockVisibilityClass, + 'getXBlockListTypeClass': getXBlockListTypeClass }; }); diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 31c07552160c..ae89196f360d 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -21,6 +21,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ var XBlockOutlineView = BaseView.extend({ // takes XBlockInfo as a model + options: { + collapsedClass: 'is-collapsed' + }, + templateName: 'xblock-outline', initialize: function() { @@ -64,6 +68,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ } html = this.template({ xblockInfo: xblockInfo, + visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(xblockInfo.get('visibility_state')), + typeListClass: XBlockViewUtils.getXBlockListTypeClass(xblockType), parentInfo: this.parentInfo, xblockType: xblockType, parentType: parentType, @@ -94,8 +100,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ this.renderedChildren = true; }, + getListElement: function() { + return this.$('> .outline-content > ol'); + }, + addChildView: function(childView) { - this.$('> .sortable-list').append(childView.$el); + this.getListElement().append(childView.$el); }, addNameEditor: function() { @@ -136,7 +146,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ addButtonActions: function(element) { var self = this; element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this)); - element.find('.add-button').click(_.bind(this.handleAddEvent, this)); + element.find('.button-new').click(_.bind(this.handleAddEvent, this)); }, shouldRenderChildren: function() { @@ -163,7 +173,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ xblockType = 'section'; } else if (category === 'sequential') { xblockType = 'subsection'; - } else if (category === 'vertical' && parentInfo && parentInfo.get('category') === 'sequential') { + } else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) { xblockType = 'unit'; } return xblockType; @@ -192,9 +202,13 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ } else { locatorElement = this.$('.outline-item[data-locator="' + locatorToShow + '"]'); } - ViewUtils.setScrollOffset(locatorElement, scrollOffset); + if (locatorElement.length > 0) { + ViewUtils.setScrollOffset(locatorElement, scrollOffset); + } else { + console.error("Failed to show item with locator " + locatorToShow + ""); + } if (editDisplayName) { - locatorElement.find('> .wrapper-xblock-header .xblock-field-value-edit').click(); + locatorElement.find('> div[class$="header"] .xblock-field-value-edit').click(); } } this.initialState = null; diff --git a/cms/static/js/views/xblock_string_field_editor.js b/cms/static/js/views/xblock_string_field_editor.js index 4002bb0be9d6..ad9de8fdaa6e 100644 --- a/cms/static/js/views/xblock_string_field_editor.js +++ b/cms/static/js/views/xblock_string_field_editor.js @@ -13,6 +13,7 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], 'click .xblock-field-value-edit': 'showInput', 'click button[name=submit]': 'onClickSubmit', 'click button[name=cancel]': 'onClickCancel', + 'click .xblock-string-field-editor': 'onClickEditor', 'change .xblock-field-input': 'updateField', 'focusout .xblock-field-input': 'onInputFocusLost', 'keyup .xblock-field-input': 'handleKeyUp' @@ -54,14 +55,20 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], onClickSubmit: function(event) { event.preventDefault(); + event.stopPropagation(); this.updateField(); }, onClickCancel: function(event) { event.preventDefault(); + event.stopPropagation(); this.cancelInput(); }, + onClickEditor: function(event) { + event.stopPropagation(); + }, + onChangeField: function() { var value = this.model.get(this.fieldName); this.getLabel().text(value); @@ -72,8 +79,9 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], showInput: function(event) { var input = this.getInput(); event.preventDefault(); + event.stopPropagation(); this.$el.addClass('is-editing'); - input.focus(); + input.focus().select(); }, hideInput: function() { diff --git a/cms/static/sass/_developer.scss b/cms/static/sass/_developer.scss index 30859b4094b3..f5c69e8e6b77 100644 --- a/cms/static/sass/_developer.scss +++ b/cms/static/sass/_developer.scss @@ -8,78 +8,3 @@ // } // -------------------- - -//.wrapper-xblock-header { - -.view-outline { - - .add-xblock-component { - text-align: center; - - .add-button { - padding: 5px 10px; - background-color: $blue; - color: $white; - text-align: center; - } - } - - .draggable-drop-indicator { - left: 0; - } - - .nav-actions { - .collapse-all { - .expand-all { - display: none; - } - } - - .expand-all { - .collapse-all { - display: none; - } - } - } - - .outline-item { - padding: 6px 8px 8px 16px; - text-wrap: avoid; - border: 1px solid $gray; - margin: 5px; - background-color: $white; - - .wrapper-xblock-header-secondary { - padding: 0px 8px 0px 26px; - - .meta-info { - font-size: 12px; - } - } - - .xblock-title { - width: 100%; - } - - .actions-list { - .action-item { - display: inline-block; - } - } - } - - .outline-item.collapsed { - .sortable-list, - .add-xblock-component { - display: none; - } - } - - .item-actions { - .configure-button { - float: left; - margin-right: 13px; - color: #a4aab7; - } - } -} diff --git a/cms/static/sass/_shame.scss b/cms/static/sass/_shame.scss index aaf088e36253..dbd6e61f9780 100644 --- a/cms/static/sass/_shame.scss +++ b/cms/static/sass/_shame.scss @@ -94,3 +94,19 @@ body b { div.wrapper-comp-editor.is-inactive ~ div.launch-latex-compiler{ display: none; } + +// ==================== + +// masthead button hidden states +.mast.has-actions .nav-actions .button.is-hidden { + @extend .is-hidden; +} + +// ==================== + +// TODOs: + +// * font-weight syncing +// * divorce display: inline-block from base button definitions/placeholders +// * canned typography class syncing + diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss index ce29dc96dc89..84cd91407b9b 100644 --- a/cms/static/sass/_variables.scss +++ b/cms/static/sass/_variables.scss @@ -160,6 +160,17 @@ $shadow-l2: rgba($black, 0.05); $shadow-d1: rgba($black, 0.4); $shadow-d2: rgba($black, 0.6); +// colors - application +$color-draft: $gray-l3; +$color-live: $blue; +$color-ready: $green; +$color-warning: $orange-l2; +$color-error: $red-l2; +$color-staff-only: $black; + +$color-heading-base: $gray-d2; +$color-copy-base: $gray-l1; + // ==================== // timing - used for animation/transition mixin syncing diff --git a/cms/static/sass/assets/_anims.scss b/cms/static/sass/assets/_anims.scss index 4726776d3998..cf3f49e59bd2 100644 --- a/cms/static/sass/assets/_anims.scss +++ b/cms/static/sass/assets/_anims.scss @@ -247,3 +247,27 @@ %anim-flashDouble { @include animation(flashDouble $tmg-f1 ease-in-out 1); } + + +// ==================== + + +// pulse +@include keyframes(pulse) { + 0% { + opacity: 0.0; + } + + 50% { + opacity: 1.0; + } + + 100% { + opacity: 0.0; + } +} + +// canned animation - use if you want out of the box/non-customized anim +%anim-pulse { + @include animation(pulse $tmg-f1 ease-in-out 1); +} diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss index 163c128dfff2..f0f7179cb7e5 100644 --- a/cms/static/sass/elements/_controls.scss +++ b/cms/static/sass/elements/_controls.scss @@ -279,7 +279,7 @@ } } -// UI: elem is collapsible +// UI: elem is collapsible - TODO: this should be transitioned away from in favor of %ui-expand-collapse %expand-collapse { @include transition(all $tmg-f2 linear 0s); display: inline-block; @@ -305,6 +305,41 @@ } } +// UI: expand collapse +%ui-expand-collapse { + @include transition(all $tmg-f2 linear 0s); + + + // CASE: default (is expanded) + .ui-toggle-expansion { + @include transition(all $tmg-f2 ease-in-out 0s); + display: inline-block; + vertical-align: middle; + + .icon { + @include transition(all $tmg-f2 ease-in-out 0s); + } + + // STATE: hover/active + &:hover, &:active { + cursor: pointer; + color: $ui-link-color-focus; + } + } + + // CASE: is collapsed + &.is-collapsed { + + .ui-toggle-expansion { + + .icon { + @include transform(rotate(-90deg)); + @include transform-origin(50% 50%); + } + } + } +} + // UI: drag handles .drag-handle { diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss index 1c1ff8eef0ad..da648a644752 100644 --- a/cms/static/sass/elements/_forms.scss +++ b/cms/static/sass/elements/_forms.scss @@ -327,8 +327,9 @@ form[class^="create-"] { } -// form - inline xblock name edit on unit, container, outline? +// form - inline xblock name edit on unit, container, outline +// TOOD: abstract this out into a Sass placeholder .incontext-editor.is-editable { .incontext-editor-value, diff --git a/cms/static/sass/elements/_layout.scss b/cms/static/sass/elements/_layout.scss index cbf615836bc4..942a85e7ef4b 100644 --- a/cms/static/sass/elements/_layout.scss +++ b/cms/static/sass/elements/_layout.scss @@ -77,7 +77,9 @@ vertical-align: baseline; } - &.new-button { + // CASE: new/create button + &.new-button, + &.button-new { @extend %btn-primary-green; @extend %sizing; } diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss index 725caf5e47dd..21edc5b5edd3 100644 --- a/cms/static/sass/elements/_modules.scss +++ b/cms/static/sass/elements/_modules.scss @@ -3,6 +3,7 @@ // Patterns for pieces of content - modules - used throughout the app // basic gray module with a strong top border and title, with related content box attached +// -------------------- %bar-module { margin-bottom: $baseline; border-top: 5px solid $gray-l1; @@ -37,6 +38,7 @@ } // blue bar and title bg version +// -------------------- %bar-module-blue { @extend %bar-module; border-top: 5px solid $blue; @@ -47,6 +49,7 @@ } // green bar and title bg version +// -------------------- %bar-module-green { @extend %bar-module; border-top: 5px solid $green; @@ -57,6 +60,7 @@ } // yellow bar and title bg version +// -------------------- %bar-module-yellow { @extend %bar-module; border-top: 5px solid $orange-l2; @@ -67,6 +71,7 @@ } // red bar and title bg version +// -------------------- %bar-module-red { @extend %bar-module; border-top: 5px solid $red-l2; @@ -88,6 +93,7 @@ // Add new component menu with big green buttons // outermost wrapper for add a new component menu +// -------------------- .add-xblock-component { margin: $baseline ($baseline/2); border: 1px solid $gray-l3; @@ -146,6 +152,7 @@ // outer most wrapper div for scroll up component picker menus // swaps in when a green button is clicked + // -------------------- .new-component-templates { @include clearfix; display: none; @@ -175,6 +182,7 @@ } // individual menus + // -------------------- .new-component-template { @include clearfix; @@ -207,6 +215,7 @@ } // basic and advanced problem tabs - also handled by jquery-ui tabs + // -------------------- .problem-type-tabs { @include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); list-style-type: none; @@ -252,4 +261,291 @@ } } +// outline UI +// -------------------- + +// outline: utilities +$outline-indent-width: $baseline; + +// UI: section +%outline-section { + @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s); + border-left: 1px solid $color-draft; + margin-bottom: $baseline; + padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4); + + // STATE: is-collapsed + &.is-collapsed { + border-left-width: ($baseline/4); + padding-left: $baseline; + + // CASE: is ready to be live + &.is-ready { + border-left-color: $color-ready; + } + + // CASE: is live + &.is-live { + border-left-color: $color-live; + } + + // CASE: has staff-only content + &.is-staff-only { + border-left-color: $color-staff-only; + } + + // CASE: has unpublished content + &.has-warnings { + border-left-color: $color-warning; + } + + // CASE: has errors + &.has-errors { + border-left-color: $color-error; + } + } +} + +// UI: subsection +%outline-subsection { + @include transition(border-left-color $tmg-f2 linear 0s); + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; + border-left: ($baseline/4) solid $color-draft; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: ($baseline*0.75); + + // CASE: is ready to be live + &.is-ready { + border-left-color: $color-ready; + } + + // CASE: is live + &.is-live { + border-left-color: $color-live; + } + + // CASE: is presented for staff only + &.is-staff-only { + border-left-color: $color-staff-only; + } + + // CASE: has unpublished content + &.has-warnings { + border-left-color: $color-warning; + } + + // CASE: has errors + &.has-errors { + border-left-color: $color-error; + } +} + +%outline-item { + + // UI: item title + .item-title { + @include transition(color $tmg-f2 ease-in-out 0s); + } + + // CASE: last-child in UI + &:last-child { + margin-bottom: 0; + } + + // CASE: has staff-only content + &.is-staff-only { + + // needed to make sure direct children only + > .section-status, + > .subsection-status, + > .unit-status { + + .icon { + color: $color-staff-only; + } + } + } + + // CASE: has unpublished content + &.has-warnings { + + // needed to make sure direct children only + > .section-status .status-message, + > .subsection-status .status-message, + > .unit-status .status-message { + + .icon { + color: $color-warning; + } + } + } + + // CASE: has errors + &.has-errors { + + // needed to make sure direct children only + > .section-status .status-message, + > .subsection-status .status-message, + > .unit-status .status-message, + > .section-status .status-message-copy, + > .subsection-status .status-message-copy, + > .unit-status .status-message-copy { + color: $color-error; + } + } +} + +%outline-item-status { + @extend %t-copy-sub2; + @extend %t-strong; + color: $color-copy-base; + + .icon { + @extend %t-icon5; + margin-right: ($baseline/4); + } +} + +// outline: sections +.outline-section { + @extend %ui-window; + @extend %outline-item; + @extend %outline-section; + + // header - title + .section-title { + @extend %t-title5; + @extend %t-strong; + color: $color-heading-base; + } + + // status + .section-status { + @extend %outline-item-status; + } + + // status - release + .status-release { + @include transition(opacity $tmg-f2 ease-in-out 0s); + opacity: 0.65; + } + + // status - grading + .status-grading { + @include transition(opacity $tmg-f2 ease-in-out 0s); + opacity: 0.65; + } + + .status-grading-value { + display: inline-block; + vertical-align: middle; + } + + .status-grading-date { + display: inline-block; + vertical-align: middle; + margin-left: ($baseline/4); + } + + // status - message + .status-message { + margin-top: ($baseline/2); + border-top: 1px solid $gray-l4; + padding-top: ($baseline/4); + + .icon { + margin-right: ($baseline/4); + } + } + + .status-message-copy { + display: inline-block; + color: $color-heading-base; + } + + // STATE: hover/active + &:hover, &:active { + + // status - release + > .section-status .status-release { + opacity: 1.0; + } + } +} + +// outline: subsections +.outline-subsection { + @extend %outline-item; + @extend %outline-subsection; + border: 1px solid $gray-l4; + border-left: ($baseline/4) solid $color-draft; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + padding: ($baseline*0.75); + + // STATE: hover/active + &:hover, &:active { + box-shadow: 0 1px 1px $shadow-l2; + } + + // STATE: is-collapsed + &.is-collapsed { + + } + + // header - title + .subsection-title { + @extend %t-title6; + color: $color-heading-base; + } + + // status + .subsection-status { + @extend %outline-item-status; + } + + // STATE: hover/active + &:hover, &:active { + + // status - release + > .subsection-status .status-release { + opacity: 1.0; + } + + // status - grading + > .subsection-status .status-grading { + opacity: 1.0; + } + } +} + +// outline: units +.outline-unit { + @extend %outline-item; + margin-bottom: ($baseline/2); + border: 1px solid $gray-l4; + padding: ($baseline/4) ($baseline/2); + + // header - title + .unit-title { + @extend %t-title7; + color: $color-heading-base; + } + + .unit-status { + @extend %outline-item-status; + } + + // STATE: hover/active + &:hover, &:active { + box-shadow: 0 1px 1px $shadow-l2; + + // status - release + .unit-status .status-release { + opacity: 1.0; + } + } +} diff --git a/cms/static/sass/elements/_typography.scss b/cms/static/sass/elements/_typography.scss index 2d0ff054125d..e14d52f6d2e6 100644 --- a/cms/static/sass/elements/_typography.scss +++ b/cms/static/sass/elements/_typography.scss @@ -3,6 +3,23 @@ // Scale - (6, 7, 8, 9, 10, 11, 12, 14, 16, 18, 21, 24, 36, 48, 60, 72) +// weights +%t-ultrastrong { + font-weight: 800; +} +%t-strong { + font-weight: 600; +} +%t-regular { + font-weight: 400; +} +%t-light { + font-weight: 300; +} +%t-ultralight { + font-weight: 200; +} + // headings/titles %t-title { font-family: $f-sans-serif; diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss index a6957810a8a4..18b137d6d1a7 100644 --- a/cms/static/sass/views/_container.scss +++ b/cms/static/sass/views/_container.scss @@ -27,12 +27,21 @@ .is-editable { + // TOOD: abstract this out into a Sass placeholder .incontext-editor-input { + @include transition(box-shadow $tmg-f1 ease-in-out 0, color $tmg-f1 ease-in-out 0); @extend %t-title4; - background: none repeat scroll 0 0 white; + @extend %t-strong; + width: 100%; + background: none repeat scroll 0 0 $white; border: 0; box-shadow: 0 0 2px 2px $shadow inset; - font-weight: 600; + + // STATE: focus + &:focus { + box-shadow: 0 0 2px 2px rgba($ui-action-primary-color-focus, 0.50) inset; + color: $ui-action-primary-color-focus; + } } } } @@ -98,16 +107,27 @@ .bit-publishing { @extend %bar-module; - &.published, - &.is-published { + // CASE: content is ready to be made live + &.is-ready { @extend %bar-module-green; } - &.draft , - &.is-draft { + // CASE: content is live + &.is-live { + @extend %bar-module-blue; + } + + // CASE: content has warnings + &.has-warnings { @extend %bar-module-yellow; } + // CASE: content has erors + &.has-errors { + @extend %bar-module-red; + } + + // CASE: content is staff only &.staff-only, &.is-staff-only { @extend %bar-module-black; @@ -201,6 +221,7 @@ // location widget .unit-location { @extend %bar-module; + border-top: none; .wrapper-unit-id { diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss index 4668be32741a..12a21123814c 100644 --- a/cms/static/sass/views/_outline.scss +++ b/cms/static/sass/views/_outline.scss @@ -1,9 +1,73 @@ // studio - views - course outline // ==================== +// view-specific utilities +// -------------------- +%outline-item-header { + @include clearfix(); + line-height: 0; + + // CASE: is-editable + // TODO: abstract out + .is-editable { + + .incontext-editor-value, .incontext-editor-action-wrapper { + vertical-align: top; + } + + .incontext-editor-value { + max-width: 80%; + margin-right: ($baseline/10); + } + + .incontext-editor-open-action { + @include transition(opacity $tmg-f1 ease-in-out 0); + opacity: 0.0; + } + + .incontext-editor-form { + width: 100%; + position: relative; + top: -($baseline/4); + } + + // TOOD: abstract this out into a Sass placeholder + .incontext-editor-input { + @include transition(box-shadow $tmg-f1 ease-in-out 0, color $tmg-f1 ease-in-out 0); + width: 100%; + background: none repeat scroll 0 0 $white; + border: 0; + box-shadow: 0 0 2px 2px $shadow inset; + + // STATE: focus + &:focus { + box-shadow: 0 0 2px 2px rgba($ui-action-primary-color-focus, 0.50) inset; + color: $ui-action-primary-color-focus; + } + } + + // STATE: hover/focus + &:hover, &:focus { + + .incontext-editor-open-action { + opacity: 1.0; + } + } + } +} + +%outline-item-content-hidden { + display: none; +} + +%outline-item-content-shown { + display: block; +} + .view-outline { // page structure + // -------------------- .content-primary, .content-supplementary { @include box-sizing(border-box); @@ -14,21 +78,8 @@ width: flex-grid(9, 12); margin-right: flex-gutter(); - .no-outline-content { - @extend %ui-well; - padding: ($baseline*2); - background-color: $gray-l4; - text-align: center; - color: $gray; - - .new-button { - @include font-size(14); - margin-left: $baseline; - - [class^="icon-"] { - margin-right: ($baseline/2); - } - } + .no-content { + @extend %no-content; } } @@ -37,26 +88,39 @@ } - // page header bits - .toggle-button-sections { - @extend %t-copy-sub2; - position: relative; - display: none; - float: right; - margin-top: ($baseline/4); - color: $gray-l1; + // page header + // -------------------- + .button-toggle-expand-collapse { + + // STATE: action will collapse all + &.collapse-all { + + .expand-all { + display: none; + } - &.is-shown { - display: block; + .collapse-all { + display: block; + } } - .label { - display: inline-block; + // STATE: action will expand all + &.expand-all { + + .collapse-all { + display: none; + } + + .expand-all { + display: block; + } } } + // adding outline elements + // -------------------- - // new section, subsection, unit + // forms .new-section-name, .new-subsection-name-input { @include font-size(16); @@ -84,6 +148,7 @@ color: $gray-l1; } + // buttons .new-subsection-item, .new-unit-item { @extend %ui-btn-flat-outline; @@ -106,491 +171,269 @@ // UI: general action list styles (section and subsection) - + // -------------------- .expand-collapse { @extend %expand-collapse; - margin: 0 ($baseline/4); } - // UI: element actions list - // TODO: outline page can be updated to reflect styling from %actions-list in _controls.scss - .actions-list { - display: inline-block; - margin-bottom: 0; - } + // outline + // -------------------- + .outline { - .actions-item { - @include font-size(13); - display: inline-block; - padding: 0 ($baseline/5); - vertical-align: middle; - - .action { - min-width: ($baseline*.75); - color: $gray-l2; + // add/new items + .add-item { + margin-top: ($baseline*0.75); - &:hover, - &.is-set { - color: $blue; - visibility: visible; - } + .button-new { + @extend %ui-btn-flat-outline; + padding: ($baseline/2) $baseline; + display: block; - //reset old drag handle style - &.drag-handle { - float: none; - margin: 0; - background: transparent url(../img/drag-handles.png) right 5px no-repeat; - text-align: center; + .icon { + display: inline-block; + vertical-align: middle; + margin-right: ($baseline/2); + } } - } - } + .add-section { + margin-bottom: $baseline; + } - // section styles - .courseware-section { - @extend %ui-window; - @include transition(background $tmg-avg ease-in-out 0); - position: relative; - padding: ($baseline*1.5) $baseline $baseline $baseline; + .add-subsection { - &.collapsed { - padding-bottom: 0; } - &.collapsed .subsection-list, - .collapsed .subsection-list, - .collapsed > ol { - display: none !important; + .add-unit { + margin-left: $outline-indent-width; } + } - &.new-section { - padding: ($baseline*1.5) $baseline 0 $baseline; - - header { - @include clearfix(); - height: auto; - border-bottom: 0; + // outline: items + .outline-item { - .expand-collapse { - display: none; - } + // CASE: expand/collapse-able + &.is-collapsible { - .item-details { - width: auto; + // only select the current item's toggle expansion controls + &:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title { - .section-name { - float: none; - width: 100%; - } + // STATE: hover/active + &:hover, &:active { + color: $blue; } } } - .section { - @include clearfix(); - min-height: 65px; // needed to align with edit input - margin-bottom: 0; - border: 0; - padding: 0; - - // section name area - .item-details { - @include clearfix(); - width: 400px; - float: none; - display: inline-block; - padding: 0 0 ($baseline/2) 0; + // item: title + .item-title { - .section-name { - @include font-size(19); - margin-right: ($baseline/2); - } + // STATE: is-editable + &.is-editable { - .section-name-span { - @include transition(color $tmg-f2 linear 0s); - cursor: pointer; + // editor + + .editor { + display: block; - &:hover { - color: $blue; + .item-edit-title { + width: 100%; } } + } + } + } - .section-name-edit { - position: relative; - width: ($baseline*20); - background: $white; + // outline: sections + // -------------------- + .outline-section { - input { - @include font-size(16); - } + // header + .section-header { + @extend %outline-item-header; - .save-button { - @include blue-button; - padding: 7px $baseline 7px; - margin-right: ($baseline/4); - } - - .cancel-button { - @include white-button; - padding: 7px $baseline 7px; - } - } + .incontext-editor-input { + @extend %t-strong; + @extend %t-title5; } + } + .section-header-details { + float: left; + width: flex-grid(6, 9); - // section specific action styles - .item-actions { - position: relative; + .icon, .wrapper-section-title { display: inline-block; - float: right; - margin-bottom: ($baseline/2); - top: 0; + vertical-align: top; } - .actions-item { - padding: 0 0 0 8px; - - &:last-child { - padding-right: 4px; - } - - &.pubdate { - padding-right: 0; - } - - .action { - - &.pubdate { - visibility: hidden; - } - - &:hover, - &.is-set { - color: $blue; - visibility: visible; - } - } - - .section-published-date { - padding: ($baseline/5) ($baseline/2); - border-radius: 3px; - background: $gray-l5; - text-align: right; - - .published-status { - @include font-size(12); - margin-right: 15px; - - strong { - font-weight: bold; - } - } - - &.released .section-published-date { - background-color: transparent; - color: $gray-l1; - - a { - color: $gray-l2; + .icon { + margin-right: ($baseline/4); + } - &:hover { - color: $blue; - } - } - } - } + .wrapper-section-title { + width: flex-grid(5, 6); + line-height: 0; } } - } - - // subsection styles - .courseware-subsection { - @include clearfix(); - padding: 3px 0; + .section-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/4); + text-align: right; - &.visible { - border-left: 5px solid $green; + .actions-list { + @extend %actions-list; + @extend %t-action2; + } } - &.mixed { - border-left: 5px solid $yellow-s1; + // status + .section-status { + margin: 0 0 0 ($outline-indent-width*1.25); } - .status { - @extend %cont-text-sr; + // content + .section-content { + @extend %outline-item-content-shown; } - .section-item { - @include transition(background $tmg-avg ease-in-out 0); - @include font-size(13); - position: relative; - display: block; - background-color: $gray-l5; - padding: 6px 8px 8px 16px; - - &:hover { - background: $blue-l5; - - .item-actions { - display: block; - } - } + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; - &.editing { - background: $orange-l4; + .ui-toggle-expansion { + @extend %t-icon3; + color: $gray-l3; } } - .details { - display: block; - margin-bottom: 0; - width: 600px; + // STATE: is-collapsed + &.is-collapsed { - a { - color: $baseFontColor; + .section-content { + @extend %outline-item-content-hidden; } } } - // gradable drop down - .gradable-status { - display: inline-block; - position: relative; + // outline: subsections + // -------------------- + .list-subsections { + margin: $baseline 0 0 0; + } - .status-label { - @include font-size(12); - width: 110px; - padding: 5px 40px 5px 10px; - border-radius: 3px; - color: transparent; - text-align: right; - font-weight: bold; - line-height: 16px; - } + .outline-subsection { - .menu-toggle { - @extend %ui-depth1; - position: absolute; - top: 0; - right: 5px; - padding: 2px 5px; - color: $gray-l2; + // header + .subsection-header { + @extend %outline-item-header; - &:hover, - &.is-active { - color: $blue; - } - - &:focus { - outline: 0; + .incontext-editor-input { + @extend %t-title6; } } + .subsection-header-details { + float: left; + width: flex-grid(6, 9); - // gradable dropdown menu default - .menu { - @include font-size(12); - @include transition(opacity $tmg-f2 linear 0s); - display: none; - opacity: 0.0; - z-index: 1; - position: absolute; - top: -4px; - right: 0; - margin: 0; - box-shadow: 0 1px 2px rgba(0, 0, 0, .2); - border: 1px solid $gray-l2; - border-radius: 4px; - padding: 8px 12px; - background: $white; - - li { - width: 115px; - margin-bottom: 3px; - border-bottom: 1px solid $gray-l4; - padding-bottom: 3px; - - &:last-child { - margin-bottom: 0; - border: none; - padding-bottom: 0; - - .gradable-status-notgraded { - color: $gray; - } - } - } - - a { - color: $blue; - - &.is-selected { - font-weight: bold; - } + .icon, .wrapper-subsection-title { + display: inline-block; + vertical-align: top; } - } - // gradable dropdown state - &.is-active { - - .menu { - @extend %ui-depth3; - display: block; - opacity: 1.0; + .icon { + margin-right: ($baseline/4); } - .menu-toggle { - @extend %ui-depth4; + .wrapper-subsection-title { + width: flex-grid(5, 6); + margin-top: -($baseline/10); + line-height: 0; } } - // set state - &.is-set { + .subsection-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/4); + text-align: right; - .menu-toggle { - color: $blue; - } - - .status-label { - display: block; - color: $blue; + .actions-list { + @extend %actions-list; + @extend %t-action2; + margin-right: ($baseline/2); } } - } - - .courseware-subsection .sortable-unit-list { - margin: ($baseline/4) 0 0 0; - } - - // unit styles - .courseware-unit { - margin: -1px 0 0 ($baseline*1.75); - &.add-new-unit { - margin: 5px ($baseline*1.75) 0 ($baseline*1.75); + // status + .subsection-status { + margin: 0 0 0 $outline-indent-width; } - .section-item { - border: 0; - background-color: $white; - } - - .public-item { - color: $black; - } - - .private-item { - color: $gray-l1; + // content + .subsection-content { + @extend %outline-item-content-shown; } - .draft-item { - color: $yellow-d1; - } + // CASE: is-collapsible + &.is-collapsible { + @extend %ui-expand-collapse; - .draft-item:after, - .public-item:after, - .private-item:after { - @include font-size(9); - margin-left: 3px; - font-weight: 600; - text-transform: uppercase; + .ui-toggle-expansion { + @extend %t-icon4; + color: $gray-l3; + } } - .draft-item:after { - content: "- draft"; - } + // STATE: is-collapsed + &.is-collapsed { - .private-item:after { - content: "- private"; + .subsection-content { + @extend %outline-item-content-hidden; + } } } - - - // modal to edit section publish settings - // basic non-backbone modal-window set-up - .wrapper-modal-window { - @extend %ui-depth4; - @include transition(all $tmg-f2 ease-in-out); - visibility: hidden; - pointer-events: none; - display: none; - position: fixed; - top: 0; - overflow: scroll; - background: $black-t2; - width: 100%; - height: 100%; - text-align: center; - - &:before { - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - margin-right: -0.25em; /* Adjusts for spacing */ - } - - .modal-window { - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - position: absolute; - top: 50%; - left: 50%; - opacity: 0; - } + // outline: units + // -------------------- + .list-units { + margin: $baseline 0 0 0; } - // modal-window showing/hiding - &.modal-window-is-shown { - overflow: hidden; + .outline-unit { + margin-left: $outline-indent-width; - .wrapper-modal-window.is-shown { - visibility: visible; - pointer-events: auto; - display: block; - - .modal-window { - opacity: 1.0; - } + // header + .unit-header { + @extend %outline-item-header; } - } - - .edit-section-publish-settings { - .picker { - @include clearfix(); - - - .field { - float: left; - margin: 0 ($baseline/2) ($baseline/2); - - label, - input { - display: block; - text-align: left; - } + .unit-header-details { + float: left; + width: flex-grid(6, 9); + margin-top: ($baseline/4); + } - label { - @extend %t-copy-sub1; - margin-bottom: ($baseline/4); - font-weight: 600; - } + .unit-header-actions { + float: right; + width: flex-grid(3, 9); + margin-top: -($baseline/10); + text-align: right; - input[type="text"] { - @extend %t-copy-sub1; - } + .actions-list { + @extend %actions-list; + @extend %t-action2; } } } - - // UI: DnD - specific elems/cases - section - .courseware-section { + // UI: drag and drop: section + // -------------------- + .outline-section { .draggable-drop-indicator-before { top: -($baseline/2); @@ -613,8 +456,8 @@ } } - // UI: DnD - specific elems/cases - subsection - .courseware-subsection { + // UI: drag and drop: subsection + .outline-subsection { .draggable-drop-indicator-before { top: 0; @@ -638,8 +481,8 @@ } } - // UI: DnD - specific elems/cases - unit - .courseware-unit { + // // UI: drag and drop: unit + .outline-unit { .draggable-drop-indicator-before { top: 0; @@ -658,7 +501,7 @@ } } - // UI: DnD - specific elems/cases - empty parents splint + // UI: drag and drop: splints .ui-splint-indicator { position: relative; } diff --git a/cms/templates/container.html b/cms/templates/container.html index bf611c0a2703..1981e3191ec2 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -10,7 +10,6 @@ <%! import json -from xmodule.modulestore import PublishState from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name from django.utils.translation import ugettext as _ %> diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 41242b72290b..dbf407a2f1f3 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -47,14 +47,14 @@

${_("Page Actions")}