Commit a3590653 by Andy Armstrong Committed by cahrens

Integrate visual styling into the course outline

parent c027f90d
...@@ -203,8 +203,8 @@ def create_a_course(): ...@@ -203,8 +203,8 @@ def create_a_course():
def add_section(): def add_section():
world.css_click('.course-outline .add-button') world.css_click('.outline .button-new')
assert_true(world.is_css_present('.outline-item-section .xblock-field-value')) 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): 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(): ...@@ -241,7 +241,7 @@ def create_unit_from_course_outline():
The end result is the page where the user is editing the new unit. The end result is the page where the user is editing the new unit.
""" """
css_selectors = [ 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: for selector in css_selectors:
world.css_click(selector) world.css_click(selector)
......
...@@ -69,7 +69,7 @@ def i_add_a_section(step): ...@@ -69,7 +69,7 @@ def i_add_a_section(step):
@step(u'I press the "section" delete icon') @step(u'I press the "section" delete icon')
def i_press_the_section_delete_icon(step): 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 > .wrapper-xblock-header a.delete-button'
world.css_click(delete_locator) world.css_click(delete_locator)
...@@ -110,9 +110,9 @@ def i_click_the_collapse_expand_all_span(step, text): ...@@ -110,9 +110,9 @@ def i_click_the_collapse_expand_all_span(step, text):
@step(u'I ([^"]*) the first section$') @step(u'I ([^"]*) the first section$')
def i_collapse_expand_a_section(step, text): def i_collapse_expand_a_section(step, text):
if text == "collapse": if text == "collapse":
locator = 'section .outline-item-section .ui-toggle-expansion' locator = 'section .outline-section .ui-toggle-expansion'
elif text == "expand": elif text == "expand":
locator = 'section .outline-item-section .ui-toggle-expansion' locator = 'section .outline-section .ui-toggle-expansion'
world.css_click(locator) world.css_click(locator)
......
...@@ -66,5 +66,5 @@ def i_am_on_tab(step, tab_name): ...@@ -66,5 +66,5 @@ def i_am_on_tab(step, tab_name):
@step('I see a link for adding a new section$') @step('I see a link for adding a new section$')
def i_see_new_section_link(step): 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') assert world.css_has_text(link_css, 'New Section')
...@@ -1209,7 +1209,7 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1209,7 +1209,7 @@ class ContentStoreTest(ContentStoreTestCase):
resp = self._show_course_overview(course.id) resp = self._show_course_overview(course.id)
self.assertContains( self.assertContains(
resp, resp,
'<article class="course-outline" data-locator="{locator}" data-course-key="{course_key}">'.format( '<article class="outline" data-locator="{locator}" data-course-key="{course_key}">'.format(
locator='i4x://MITx/999/course/Robot_Super_Course', locator='i4x://MITx/999/course/Robot_Super_Course',
course_key='MITx/999/Robot_Super_Course', course_key='MITx/999/Robot_Super_Course',
), ),
......
...@@ -9,7 +9,7 @@ from django.test.client import Client ...@@ -9,7 +9,7 @@ from django.test.client import Client
from django.contrib.auth.models import User from django.contrib.auth.models import User
from xmodule.contentstore.django import contentstore from xmodule.contentstore.django import contentstore
from xmodule.modulestore import PublishState, ModuleStoreEnum from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum, mongo
from xmodule.modulestore.inheritance import own_metadata from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
...@@ -151,16 +151,16 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -151,16 +151,16 @@ class CourseTestCase(ModuleStoreTestCase):
# create a Draft vertical # create a Draft vertical
vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1)
draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id) draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id)
self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft) self.assertEqual(self.store.compute_publish_state(draft_vertical), LegacyPublishState.draft)
# create a Private (draft only) vertical # create a Private (draft only) vertical
private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL) private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL)
self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private) self.assertEqual(self.store.compute_publish_state(private_vertical), LegacyPublishState.private)
# create a Published (no draft) vertical # create a Published (no draft) vertical
public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL) public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL)
public_vertical = self.store.publish(public_vertical.location, self.user.id) public_vertical = self.store.publish(public_vertical.location, self.user.id)
self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public) self.assertEqual(self.store.compute_publish_state(public_vertical), LegacyPublishState.public)
# add the new private and new public as children of the sequential # add the new private and new public as children of the sequential
sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL)) sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL))
...@@ -197,7 +197,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -197,7 +197,7 @@ class CourseTestCase(ModuleStoreTestCase):
def verify_item_publish_state(item, publish_state): def verify_item_publish_state(item, publish_state):
"""Verifies the publish state of the item is as expected.""" """Verifies the publish state of the item is as expected."""
if publish_state in (PublishState.private, PublishState.draft): if publish_state in (LegacyPublishState.private, LegacyPublishState.draft):
self.assertTrue(getattr(item, 'is_draft', False)) self.assertTrue(getattr(item, 'is_draft', False))
else: else:
self.assertFalse(getattr(item, 'is_draft', False)) self.assertFalse(getattr(item, 'is_draft', False))
...@@ -210,18 +210,18 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -210,18 +210,18 @@ class CourseTestCase(ModuleStoreTestCase):
return item return item
# verify that the draft vertical is draft # verify that the draft vertical is draft
vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, PublishState.draft) vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, LegacyPublishState.draft)
for child in vertical.get_children(): for child in vertical.get_children():
verify_item_publish_state(child, PublishState.draft) verify_item_publish_state(child, LegacyPublishState.draft)
# make sure that we don't have a sequential that is not in draft mode # make sure that we don't have a sequential that is not in draft mode
sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, PublishState.public) sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, LegacyPublishState.public)
# verify that we have the private vertical # verify that we have the private vertical
private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, PublishState.private) private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, LegacyPublishState.private)
# verify that we have the public vertical # verify that we have the public vertical
public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, PublishState.public) public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, LegacyPublishState.public)
# verify verticals are children of sequential # verify verticals are children of sequential
for vert in [vertical, private_vertical, public_vertical]: for vert in [vertical, private_vertical, public_vertical]:
...@@ -332,7 +332,7 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -332,7 +332,7 @@ class CourseTestCase(ModuleStoreTestCase):
it'll return public in that case it'll return public in that case
""" """
supposed_state = self.store.compute_publish_state(item) supposed_state = self.store.compute_publish_state(item)
if supposed_state == PublishState.draft and isinstance(item.runtime.modulestore, DraftModuleStore): if supposed_state == LegacyPublishState.draft and isinstance(item.runtime.modulestore, DraftModuleStore):
# see if the draft differs from the published # see if the draft differs from the published
published = self.store.get_item(item.location, revision=ModuleStoreEnum.RevisionOption.published_only) published = self.store.get_item(item.location, revision=ModuleStoreEnum.RevisionOption.published_only)
if item.get_explicitly_set_fields_by_scope() != published.get_explicitly_set_fields_by_scope(): if item.get_explicitly_set_fields_by_scope() != published.get_explicitly_set_fields_by_scope():
...@@ -345,13 +345,13 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -345,13 +345,13 @@ class CourseTestCase(ModuleStoreTestCase):
# checking children: if published differs from item, return draft # checking children: if published differs from item, return draft
return supposed_state return supposed_state
# published == item in all respects, so return public # published == item in all respects, so return public
return PublishState.public return LegacyPublishState.public
elif supposed_state == PublishState.public and item.location.category in DIRECT_ONLY_CATEGORIES: elif supposed_state == LegacyPublishState.public and item.location.category in DIRECT_ONLY_CATEGORIES:
if not all([ if not all([
self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only) self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only)
for child_loc in item.children for child_loc in item.children
]): ]):
return PublishState.draft return LegacyPublishState.draft
else: else:
return supposed_state return supposed_state
else: else:
......
...@@ -155,10 +155,10 @@ def compute_publish_state(xblock): ...@@ -155,10 +155,10 @@ def compute_publish_state(xblock):
Returns whether this xblock is draft, public, or private. Returns whether this xblock is draft, public, or private.
Returns: Returns:
PublishState.draft - content is in the process of being edited, but still has a previous LegacyPublishState.draft - content is in the process of being edited, but still has a previous
version deployed to LMS version deployed to LMS
PublishState.public - content is locked and deployed to LMS LegacyPublishState.public - content is locked and deployed to LMS
PublishState.private - content is editable and not deployed to LMS LegacyPublishState.private - content is editable and not deployed to LMS
""" """
return modulestore().compute_publish_state(xblock) return modulestore().compute_publish_state(xblock)
......
...@@ -12,7 +12,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -12,7 +12,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore import PublishState from xmodule.modulestore import LegacyPublishState
from xblock.core import XBlock from xblock.core import XBlock
from xblock.django.request import webob_to_django_response, django_to_webob_request from xblock.django.request import webob_to_django_response, django_to_webob_request
...@@ -101,7 +101,7 @@ def subsection_handler(request, usage_key_string): ...@@ -101,7 +101,7 @@ def subsection_handler(request, usage_key_string):
subsection_units = item.get_children() subsection_units = item.get_children()
for unit in subsection_units: for unit in subsection_units:
state = compute_publish_state(unit) state = compute_publish_state(unit)
if state in (PublishState.public, PublishState.draft): if state in (LegacyPublishState.public, LegacyPublishState.draft):
can_view_live = True can_view_live = True
break break
......
...@@ -404,7 +404,7 @@ def course_outline_initial_state(locator_to_show, course_structure): ...@@ -404,7 +404,7 @@ def course_outline_initial_state(locator_to_show, course_structure):
""" """
if xblock_info['id'] == locator: if xblock_info['id'] == locator:
return xblock_info 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: if children:
for child_xblock_info in children: for child_xblock_info in children:
result = find_xblock_info(child_xblock_info, locator) result = find_xblock_info(child_xblock_info, locator)
...@@ -417,7 +417,7 @@ def course_outline_initial_state(locator_to_show, course_structure): ...@@ -417,7 +417,7 @@ def course_outline_initial_state(locator_to_show, course_structure):
Collect all the locators for an xblock and its children. Collect all the locators for an xblock and its children.
""" """
locators.append(xblock_info['id']) 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: if children:
for child_xblock_info in children: for child_xblock_info in children:
collect_all_locators(locators, child_xblock_info) collect_all_locators(locators, child_xblock_info)
......
...@@ -601,10 +601,6 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -601,10 +601,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 In addition, an optional include_children_predicate argument can be provided to define whether or
not a particular xblock should have its children included. 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): def safe_get_username(user_id):
""" """
...@@ -623,11 +619,23 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -623,11 +619,23 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
return None 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)
xblock_info = { xblock_info = {
"id": unicode(xblock.location), "id": unicode(xblock.location),
"display_name": xblock.display_name_with_default, "display_name": xblock.display_name_with_default,
"category": xblock.category, "category": xblock.category,
"has_changes": modulestore().has_changes(xblock.location),
"published": published, "published": published,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "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), "edited_by": safe_get_username(xblock.subtree_edited_by),
...@@ -637,8 +645,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -637,8 +645,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"released_to_students": datetime.now(UTC) > xblock.start, "released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date, "release_date": release_date,
"release_date_from": _get_release_date_from(xblock) if release_date else None, "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": currently_visible_to_students,
"currently_visible_to_students": is_currently_visible_to_students(xblock), "publish_state": _compute_publish_state(xblock, child_info) if not xblock.category == 'course' else None
} }
if data is not None: if data is not None:
xblock_info["data"] = data xblock_info["data"] = data
...@@ -646,13 +654,70 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -646,13 +654,70 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info["metadata"] = metadata xblock_info["metadata"] = metadata
if include_ancestor_info: if include_ancestor_info:
xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock) xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock)
if include_child_info and xblock.has_children: if child_info:
xblock_info['child_info'] = _create_xblock_child_info( xblock_info['child_info'] = child_info
xblock, include_children_predicate=include_children_predicate
)
return xblock_info return xblock_info
class PublishState(object):
"""
Represents the possible publish states for an xblock:
live - the block and all of its children are live to students (except for staff only items)
ready - the block and all of its children are ready to go live in the future
unscheduled - the block and all of its children are unscheduled
has_unpublished_content - the block or its children have unpublished content that is not staff only
staff_only - all of the block's content is to be shown to staff only
"""
live = 'live'
ready = 'ready'
unscheduled = 'unscheduled'
has_unpublished_content = 'has_unpublished_content'
staff_only = 'staff_only'
def _compute_publish_state(xblock, child_info):
"""
Returns the current publish state for the specified xblock and its children
"""
if xblock.visible_to_staff_only:
return PublishState.staff_only
elif is_unit(xblock) and modulestore().has_changes(xblock.location):
return PublishState.has_unpublished_content
is_unscheduled = xblock.start == DEFAULT_START_DATE
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['publish_state']
if child_state == PublishState.has_unpublished_content:
return child_state
elif not child_state == PublishState.staff_only:
all_staff_only = False
if not child_state == PublishState.unscheduled:
all_unscheduled = False
if not child_state == PublishState.live:
all_live = False
if all_staff_only:
return PublishState.staff_only
elif all_unscheduled:
if not is_unscheduled:
return PublishState.has_unpublished_content
else:
return PublishState.unscheduled
elif all_live:
return PublishState.live
else:
return PublishState.ready
if is_unscheduled:
return PublishState.unscheduled
elif datetime.now(UTC) > xblock.start:
return PublishState.live
else:
return PublishState.ready
def _create_xblock_ancestor_info(xblock): def _create_xblock_ancestor_info(xblock):
""" """
Returns information about the ancestors of an xblock. Note that the direct parent will also return Returns information about the ancestors of an xblock. Note that the direct parent will also return
......
...@@ -10,6 +10,7 @@ from contentstore.views.access import has_course_access ...@@ -10,6 +10,7 @@ from contentstore.views.access import has_course_access
from contentstore.views.course import course_outline_initial_state from contentstore.views.course import course_outline_initial_state
from course_action_state.models import CourseRerunState from course_action_state.models import CourseRerunState
from contentstore.views.item import create_xblock_info from contentstore.views.item import create_xblock_info
from contentstore.views.item import create_xblock_info, PublishState
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
...@@ -229,7 +230,7 @@ class TestCourseOutline(CourseTestCase): ...@@ -229,7 +230,7 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(json_response['category'], 'course') self.assertEqual(json_response['category'], 'course')
self.assertEqual(json_response['id'], 'i4x://MITx/999/course/Robot_Super_Course') self.assertEqual(json_response['id'], 'i4x://MITx/999/course/Robot_Super_Course')
self.assertEqual(json_response['display_name'], 'Robot Super Course') self.assertEqual(json_response['display_name'], 'Robot Super Course')
self.assertTrue(json_response['published']) self.assertIsNone(json_response['publish_state'])
# Now verify the first child # Now verify the first child
children = json_response['child_info']['children'] children = json_response['child_info']['children']
...@@ -238,7 +239,7 @@ class TestCourseOutline(CourseTestCase): ...@@ -238,7 +239,7 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(first_child_response['category'], 'chapter') self.assertEqual(first_child_response['category'], 'chapter')
self.assertEqual(first_child_response['id'], 'i4x://MITx/999/chapter/Week_1') self.assertEqual(first_child_response['id'], 'i4x://MITx/999/chapter/Week_1')
self.assertEqual(first_child_response['display_name'], 'Week 1') self.assertEqual(first_child_response['display_name'], 'Week 1')
self.assertTrue(first_child_response['published']) self.assertEqual(first_child_response['publish_state'], PublishState.unscheduled)
self.assertTrue(len(first_child_response['child_info']['children']) > 0) self.assertTrue(len(first_child_response['child_info']['children']) > 0)
# Finally, validate the entire response for consistency # Finally, validate the entire response for consistency
...@@ -251,7 +252,6 @@ class TestCourseOutline(CourseTestCase): ...@@ -251,7 +252,6 @@ class TestCourseOutline(CourseTestCase):
self.assertIsNotNone(json_response['display_name']) self.assertIsNotNone(json_response['display_name'])
self.assertIsNotNone(json_response['id']) self.assertIsNotNone(json_response['id'])
self.assertIsNotNone(json_response['category']) self.assertIsNotNone(json_response['category'])
self.assertIsNotNone(json_response['published'])
if json_response.get('child_info', None): if json_response.get('child_info', None):
for child_response in json_response['child_info']['children']: for child_response in json_response['child_info']['children']:
self.assert_correct_json_response(child_response) self.assert_correct_json_response(child_response)
......
...@@ -25,21 +25,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -25,21 +25,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/ */
"ancestor_info": null, "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. * Date of the last edit to this xblock or any of its descendants.
*/ */
"edited_on":null, "edited_on":null,
...@@ -48,6 +33,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -48,6 +33,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/ */
"edited_by":null, "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. * Date of the last publish of this xblock, or null if never published.
*/ */
"published_on": null, "published_on": null,
...@@ -56,6 +45,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu ...@@ -56,6 +45,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/ */
"published_by": null, "published_by": null,
/** /**
* Represents the possible publish states for an xblock:
* is_live - the block and all of its children are live to students (except for staff only items)
* is_ready - the block and all of its children are ready to go live in the future
* unscheduled - the block and all of its children are unscheduled
* has_unpublished_content - the block or its children have unpublished content that is not staff only
* is_staff_only - all of the block's content is to be shown to staff only
*/
"publish_state": null,
/**
* True iff the release date of the xblock is in the past. * True iff the release date of the xblock is in the past.
*/ */
"released_to_students": null, "released_to_students": null,
......
...@@ -25,12 +25,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -25,12 +25,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
category: 'vertical', category: 'vertical',
display_name: displayName, display_name: displayName,
studio_url: '/container/mock-unit', studio_url: '/container/mock-unit',
publish_state: 'unscheduled',
ancestor_info: { ancestor_info: {
ancestors: [{ ancestors: [{
id: 'mock-subsection', id: 'mock-subsection',
category: 'sequential', category: 'sequential',
display_name: 'Mock Subsection', display_name: 'Mock Subsection',
studio_url: '/course/mock-course?show=mock-subsection', studio_url: '/course/mock-course?show=mock-subsection',
publish_state: 'unscheduled',
child_info: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
...@@ -38,24 +40,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -38,24 +40,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
id: 'mock-unit', id: 'mock-unit',
category: 'vertical', category: 'vertical',
display_name: displayName, display_name: displayName,
studio_url: '/container/mock-unit' studio_url: '/container/mock-unit',
publish_state: 'unscheduled'
}, { }, {
id: 'mock-unit-2', id: 'mock-unit-2',
category: 'vertical', category: 'vertical',
display_name: 'Mock Unit 2', display_name: 'Mock Unit 2',
studio_url: '/container/mock-unit-2' studio_url: '/container/mock-unit-2',
publish_state: 'unscheduled'
}] }]
} }
}, { }, {
id: 'mock-section', id: 'mock-section',
category: 'chapter', category: 'chapter',
display_name: 'Section', display_name: 'Section',
studio_url: '/course/slashes:mock-course?show=mock-section' studio_url: '/course/slashes:mock-course?show=mock-section',
publish_state: 'unscheduled'
}, { }, {
id: 'mock-course', id: 'mock-course',
category: 'course', category: 'course',
display_name: 'Mock Course', display_name: 'Mock Course',
studio_url: '/course/mock-course' studio_url: '/course/mock-course',
publish_state: 'unscheduled'
}] }]
}, },
metadata: { metadata: {
...@@ -77,16 +83,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -77,16 +83,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can render itself', function() { it('can render itself', function() {
createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
expect(unitOutlineView.$('.sortable-course-list')).toExist(); expect(unitOutlineView.$('.list-sections')).toExist();
expect(unitOutlineView.$('.sortable-section-list')).toExist(); expect(unitOutlineView.$('.list-subsections')).toExist();
expect(unitOutlineView.$('.sortable-subsection-list')).toExist(); expect(unitOutlineView.$('.list-units')).toExist();
}); });
it('can add a unit', function() { it('can add a unit', function() {
var redirectSpy; var redirectSpy;
createUnitOutlineView(this, createMockXBlockInfo('Mock Unit')); createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
redirectSpy = spyOn(ViewUtils, 'redirect'); 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/', { create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
...@@ -106,8 +112,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -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.expectJsonRequest(requests, 'GET', '/xblock/mock-unit');
create_sinon.respondWithJson(requests, create_sinon.respondWithJson(requests,
createMockXBlockInfo(updatedDisplayName)); createMockXBlockInfo(updatedDisplayName));
unitHeader = unitOutlineView.$('.outline-item-unit .wrapper-xblock-header'); expect(unitOutlineView.$('.outline-unit .unit-title').first().text().trim()).toBe(updatedDisplayName);
expect(unitHeader.find('.xblock-title').first().text().trim()).toBe(updatedDisplayName);
}); });
}); });
}); });
...@@ -16,6 +16,10 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b ...@@ -16,6 +16,10 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
"click .ui-toggle-expansion": "toggleExpandCollapse" "click .ui-toggle-expansion": "toggleExpandCollapse"
}, },
options: {
collapsedClass: 'collapsed'
},
//override the constructor function //override the constructor function
constructor: function(options) { constructor: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender'); _.bindAll(this, 'beforeRender', 'render', 'afterRender');
...@@ -48,7 +52,7 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b ...@@ -48,7 +52,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. // this element, e.g. clicking on the element of a child view container in a parent.
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
ViewUtils.toggleExpandCollapse(target); ViewUtils.toggleExpandCollapse(target, this.options.collapsedClass);
}, },
/** /**
......
...@@ -12,6 +12,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -12,6 +12,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
var XBlockContainerPage = BasePage.extend({ var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
options: {
collapsedClass: 'is-collapsed'
},
view: 'container_preview', view: 'container_preview',
initialize: function(options) { initialize: function(options) {
......
...@@ -7,7 +7,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -7,7 +7,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var disabledCss = "is-disabled"; var 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. * after a server sync operation.
*/ */
var ContainerStateListenerView = BaseView.extend({ var ContainerStateListenerView = BaseView.extend({
...@@ -53,19 +53,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -53,19 +53,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
*/ */
var PreviewActionController = ContainerStateListenerView.extend({ var PreviewActionController = ContainerStateListenerView.extend({
shouldRefresh: function(model) { shouldRefresh: function(model) {
return ViewUtils.hasChangedAttributes(model, ['has_changes', 'published']); return ViewUtils.hasChangedAttributes(model, ['edited_on', 'published_on', 'publish_state']);
}, },
render: function() { render: function() {
var previewAction = this.$el.find('.button-preview'), var previewAction = this.$el.find('.button-preview'),
viewLiveAction = this.$el.find('.button-view'); viewLiveAction = this.$el.find('.button-view'),
if (this.model.get('published')) { publishState = this.model.get('publish_state');
if (publishState !== 'unscheduled') {
viewLiveAction.removeClass(disabledCss); viewLiveAction.removeClass(disabledCss);
} }
else { else {
viewLiveAction.addClass(disabledCss); viewLiveAction.addClass(disabledCss);
} }
if (this.model.get('has_changes') || !this.model.get('published')) { if (publishState !== 'live' && publishState !== 'ready') {
previewAction.removeClass(disabledCss); previewAction.removeClass(disabledCss);
} }
else { else {
...@@ -98,25 +99,22 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -98,25 +99,22 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
}, },
onSync: function(model) { onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, [ if (ViewUtils.hasChangedAttributes(model, [ 'edited_on', 'published_on', 'publish_state' ])) {
'has_changes', 'published', 'edited_on', 'edited_by', 'visible_to_staff_only'
])) {
this.render(); this.render();
} }
}, },
render: function () { render: function () {
this.$el.html(this.template({ this.$el.html(this.template({
hasChanges: this.model.get('has_changes'), publishState: this.model.get('publish_state'),
published: this.model.get('published'),
editedOn: this.model.get('edited_on'), editedOn: this.model.get('edited_on'),
editedBy: this.model.get('edited_by'), editedBy: this.model.get('edited_by'),
published: this.model.get('published'),
publishedOn: this.model.get('published_on'), publishedOn: this.model.get('published_on'),
publishedBy: this.model.get('published_by'), publishedBy: this.model.get('published_by'),
releasedToStudents: this.model.get('released_to_students'), releasedToStudents: this.model.get('released_to_students'),
releaseDate: this.model.get('release_date'), releaseDate: this.model.get('release_date'),
releaseDateFrom: this.model.get('release_date_from'), releaseDateFrom: this.model.get('release_date_from')
visibleToStaffOnly: this.model.get('visible_to_staff_only')
})); }));
return this; return this;
...@@ -138,7 +136,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -138,7 +136,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
}, },
discardChanges: function (e) { discardChanges: function (e) {
var xblockInfo = this.model, that=this, renderPage = this.renderPage; var xblockInfo = this.model, renderPage = this.renderPage;
if (e && e.preventDefault) { if (e && e.preventDefault) {
e.preventDefault(); e.preventDefault();
} }
...@@ -164,7 +162,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -164,7 +162,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
if (e && e.preventDefault) { if (e && e.preventDefault) {
e.preventDefault(); e.preventDefault();
} }
enableStaffLock = !xblockInfo.get('visible_to_staff_only'); enableStaffLock = xblockInfo.get('publish_state') !== 'staff_only';
revertCheckBox = function() { revertCheckBox = function() {
self.checkStaffLock(!enableStaffLock); self.checkStaffLock(!enableStaffLock);
...@@ -223,7 +221,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -223,7 +221,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
}, },
onSync: function(model) { onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, ['published', 'published_on', 'published_by'])) { if (ViewUtils.hasChangedAttributes(model, ['published_on'])) {
this.render(); this.render();
} }
}, },
......
...@@ -8,14 +8,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -8,14 +8,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// takes XBlockInfo as a model // takes XBlockInfo as a model
events: { events: {
"click .toggle-button-expand-collapse": "toggleExpandCollapse" "click .button-toggle-expand-collapse": "toggleExpandCollapse"
},
options: {
collapsedClass: 'is-collapsed'
}, },
initialize: function() { initialize: function() {
var self = this; var self = this;
this.initialState = this.options.initialState; this.initialState = this.options.initialState;
BasePage.prototype.initialize.call(this); BasePage.prototype.initialize.call(this);
this.$('.add-button').click(function(event) { this.$('.button-new').click(function(event) {
self.outlineView.handleAddEvent(event); self.outlineView.handleAddEvent(event);
}); });
this.model.on('change', this.setCollapseExpandVisibility, this); this.model.on('change', this.setCollapseExpandVisibility, this);
...@@ -23,19 +27,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -23,19 +27,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
setCollapseExpandVisibility: function() { setCollapseExpandVisibility: function() {
var has_content = this.hasContent(), var has_content = this.hasContent(),
collapseExpandButton = $('.toggle-button-expand-collapse'); collapseExpandButton = $('.button-toggle-expand-collapse');
if (has_content) { if (has_content) {
collapseExpandButton.show(); collapseExpandButton.removeClass('is-hidden');
} else { } else {
collapseExpandButton.hide(); collapseExpandButton.addClass('is-hidden');
} }
}, },
renderPage: function() { renderPage: function() {
var locatorToShow;
this.setCollapseExpandVisibility(); this.setCollapseExpandVisibility();
this.outlineView = new CourseOutlineView({ this.outlineView = new CourseOutlineView({
el: this.$('.course-outline'), el: this.$('.outline'),
model: this.model, model: this.model,
isRoot: true, isRoot: true,
initialState: this.initialState initialState: this.initialState
...@@ -50,19 +53,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views ...@@ -50,19 +53,16 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
}, },
toggleExpandCollapse: function(event) { toggleExpandCollapse: function(event) {
var toggleButton = this.$('.toggle-button-expand-collapse'), var toggleButton = this.$('.button-toggle-expand-collapse'),
collapse = toggleButton.hasClass('collapse-all'); collapse = toggleButton.hasClass('collapse-all');
event.preventDefault(); event.preventDefault();
toggleButton.toggleClass('collapse-all expand-all'); toggleButton.toggleClass('collapse-all expand-all');
this.$('.course-outline > ol > li').each(function(index, domElement) { this.$('.list-sections > li').each(function(index, domElement) {
var element = $(domElement), var element = $(domElement);
expandCollapseElement = element.find('.expand-collapse').first();
if (collapse) { if (collapse) {
expandCollapseElement.removeClass('expand').addClass('collapse'); element.addClass('is-collapsed');
element.addClass('collapsed');
} else { } else {
expandCollapseElement.addClass('expand').removeClass('collapse'); element.removeClass('is-collapsed');
element.removeClass('collapsed');
} }
}); });
} }
......
...@@ -10,6 +10,7 @@ define(['js/views/xblock_outline'], ...@@ -10,6 +10,7 @@ define(['js/views/xblock_outline'],
// takes XBlockInfo as a model // takes XBlockInfo as a model
templateName: 'unit-outline', templateName: 'unit-outline',
className: 'group-configurations-list',
render: function() { render: function() {
XBlockOutlineView.prototype.render.call(this); XBlockOutlineView.prototype.render.call(this);
...@@ -23,7 +24,7 @@ define(['js/views/xblock_outline'], ...@@ -23,7 +24,7 @@ define(['js/views/xblock_outline'],
previousAncestor = null; previousAncestor = null;
if (this.model.get('ancestor_info')) { if (this.model.get('ancestor_info')) {
ancestors = this.model.get('ancestor_info').ancestors; 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 // 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, // start at the root, but the ancestors are ordered by closeness to the unit,
// i.e. subsection and then section. // i.e. subsection and then section.
...@@ -33,7 +34,7 @@ define(['js/views/xblock_outline'], ...@@ -33,7 +34,7 @@ define(['js/views/xblock_outline'],
ancestorView.render(); ancestorView.render();
listElement.append(ancestorView.$el); listElement.append(ancestorView.$el);
previousAncestor = ancestor; previousAncestor = ancestor;
listElement = ancestorView.$('.sortable-list'); listElement = ancestorView.getListElement();
} }
} }
return ancestorView; return ancestorView;
......
...@@ -10,9 +10,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js ...@@ -10,9 +10,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
/** /**
* Toggles the expanded state of the current element. * 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('.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(); target.closest('.is-collapsible').children('article').slideToggle();
}; };
......
...@@ -21,6 +21,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -21,6 +21,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var XBlockOutlineView = BaseView.extend({ var XBlockOutlineView = BaseView.extend({
// takes XBlockInfo as a model // takes XBlockInfo as a model
options: {
collapsedClass: 'is-collapsed'
},
templateName: 'xblock-outline', templateName: 'xblock-outline',
initialize: function() { initialize: function() {
...@@ -94,8 +98,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -94,8 +98,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
this.renderedChildren = true; this.renderedChildren = true;
}, },
getListElement: function() {
return this.$('> .outline-content > ol');
},
addChildView: function(childView) { addChildView: function(childView) {
this.$('> .sortable-list').append(childView.$el); this.getListElement().append(childView.$el);
}, },
addNameEditor: function() { addNameEditor: function() {
...@@ -136,7 +144,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -136,7 +144,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
addButtonActions: function(element) { addButtonActions: function(element) {
var self = this; var self = this;
element.find('.delete-button').click(_.bind(this.handleDeleteEvent, 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() { shouldRenderChildren: function() {
...@@ -163,7 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/ ...@@ -163,7 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
xblockType = 'section'; xblockType = 'section';
} else if (category === 'sequential') { } else if (category === 'sequential') {
xblockType = 'subsection'; xblockType = 'subsection';
} else if (category === 'vertical' && parentInfo && parentInfo.get('category') === 'sequential') { } else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
xblockType = 'unit'; xblockType = 'unit';
} }
return xblockType; return xblockType;
......
...@@ -318,7 +318,7 @@ $outline-indent-width: $baseline; ...@@ -318,7 +318,7 @@ $outline-indent-width: $baseline;
border-left-color: $color-live; border-left-color: $color-live;
} }
// CASE: has staff-only content // CASE: is presented for staff only
&.is-staff-only { &.is-staff-only {
border-left-color: $color-staff-only; border-left-color: $color-staff-only;
} }
......
...@@ -25,14 +25,15 @@ ...@@ -25,14 +25,15 @@
font-weight: 600; font-weight: 600;
} }
// TODO: abstract out
.is-editable { .is-editable {
.incontext-editor-input { .incontext-editor-input {
@extend %t-title4; @extend %t-title4;
background: none repeat scroll 0 0 white; @extend %t-strong;
background: none repeat scroll 0 0 $white;
border: 0; border: 0;
box-shadow: 0 0 2px 2px $shadow inset; box-shadow: 0 0 2px 2px $shadow inset;
font-weight: 600;
} }
} }
} }
......
...@@ -6,6 +6,37 @@ ...@@ -6,6 +6,37 @@
%outline-item-header { %outline-item-header {
@include clearfix(); @include clearfix();
line-height: 0; line-height: 0;
// CASE: is-editable
// TODO: abstract out
.is-editable {
.incontext-editor-open-action {
@include transition(opacity $tmg-f1 ease-in-out 0);
opacity: 0.0;
}
.incontext-editor-form {
width: 100%;
}
.incontext-editor-input {
@extend %t-title5;
@extend %t-strong;
width: 100%;
background: none repeat scroll 0 0 $white;
border: 0;
box-shadow: 0 0 2px 2px $shadow-l1 inset;
}
// STATE: hover/focus
&:hover, &:focus {
.incontext-editor-open-action {
opacity: 1.0;
}
}
}
} }
%outline-item-content-hidden { %outline-item-content-hidden {
......
...@@ -10,7 +10,6 @@ else: ...@@ -10,7 +10,6 @@ else:
<%! <%!
import json import json
from xmodule.modulestore import PublishState
from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name from contentstore.views.helpers import xblock_studio_url, xblock_type_display_name
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
%> %>
......
...@@ -47,13 +47,13 @@ from contentstore.utils import reverse_usage_url ...@@ -47,13 +47,13 @@ from contentstore.utils import reverse_usage_url
<h3 class="sr">${_("Page Actions")}</h3> <h3 class="sr">${_("Page Actions")}</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="toggle-button toggle-button-expand-collapse collapse-all is-hidden"> <a href="#" class="button button-toggle button-toggle-expand-collapse collapse-all is-hidden">
<span class="collapse-all"><i class="icon-arrow-up"></i> <span class="label">${_("Collapse All Sections")}</span></span> <span class="collapse-all"><i class="icon-arrow-up"></i> <span class="label">${_("Collapse All Sections")}</span></span>
<span class="expand-all"><i class="icon-arrow-down"></i> <span class="label">${_("Expand All Sections")}</span></span> <span class="expand-all"><i class="icon-arrow-down"></i> <span class="label">${_("Expand All Sections")}</span></span>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button view-button add-button" data-category="chapter" data-parent="${context_course.location}" data-default-name="Section"> <a href="#" class="button button-new" data-category="chapter" data-parent="${context_course.location}" data-default-name="Section">
<i class="icon-plus"></i>${_('New Section')} <i class="icon-plus"></i>${_('New Section')}
</a> </a>
</li> </li>
...@@ -72,7 +72,7 @@ from contentstore.utils import reverse_usage_url ...@@ -72,7 +72,7 @@ from contentstore.utils import reverse_usage_url
<% <%
course_locator = context_course.location course_locator = context_course.location
%> %>
<article class="course-outline" data-locator="${course_locator}" data-course-key="${course_locator.course_key}"> <article class="outline" data-locator="${course_locator}" data-course-key="${course_locator.course_key}">
</article> </article>
</div> </div>
<div class="ui-loading"> <div class="ui-loading">
......
<%
var category = xblockInfo.get('category');
var releasedToStudents = xblockInfo.get('released_to_students');
var publishState = xblockInfo.get('publish_state');
var publishClass = '';
if (publishState === 'staff_only') {
publishClass = 'is-staff-only';
} else if (publishState === 'live') {
publishClass = 'is-live';
} else if (publishState === 'ready') {
publishClass = 'is-ready';
} else if (publishState === 'has_unpublished_content') {
publishClass = 'has-warnings';
}
var listType = 'list-unknown';
if (xblockType === 'course') {
listType = 'list-sections';
} else if (xblockType === 'section') {
listType = 'list-subsections';
} else if (xblockType === 'subsection') {
listType = 'list-units';
}
var statusMessage = null;
var statusType = null;
if (publishState === 'is_staff_only') {
statusType = 'staff-only';
statusMessage = 'Contains staff only content';
} else if (publishState === 'has_unpublished_content') {
if (category === 'vertical') {
statusType = 'warning';
if (releasedToStudents) {
statusMessage = 'Unpublished changes to live content';
} else {
statusMessage = 'Unpublished units will not be released';
}
}
}
var statusIconClass = '';
if (statusType === 'warning') {
statusIconClass = 'icon-file-alt';
} else if (statusType === 'error') {
statusIconClass = 'icon-warning-sign';
} else if (statusType === 'staff-only') {
statusIconClass = 'icon-lock';
}
%>
<% if (parentInfo) { %> <% if (parentInfo) { %>
<li class="outline-item outline-item-<%= xblockType %> <%= includesChildren ? 'is-collapsible' : '' %> is-draggable <%= isCollapsed ? 'collapsed' : '' %>" <li class="outline-item outline-<%= xblockType %> <%= publishClass %> is-draggable <%= includesChildren ? 'is-collapsible' : '' %> <%= isCollapsed ? 'is-collapsed' : '' %>"
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>"> data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
<div class="wrapper-xblock-header"> <div class="<%= xblockType %>-header">
<div class="wrapper-xblock-header-primary"> <% if (includesChildren) { %>
<% if (includesChildren) { %> <h3 class="<%= xblockType %>-header-details expand-collapse <%= isCollapsed ? 'expand' : 'collapse' %> ui-toggle-expansion" title="<%= gettext('Collapse/Expand this Checklist') %>">
<h3 class="xblock-title expand-collapse <%= isCollapsed ? 'expand' : 'collapse' %>" title="<%= gettext('Collapse/Expand this Checklist') %>"> <i class="icon-caret-down icon"></i>
<i class="icon-caret-down ui-toggle-expansion"></i> <% } else { %>
<% } else { %> <h3 class="<%= xblockType %>-header-details">
<h3 class="xblock-title"> <% } %>
<% } %>
<% if (xblockInfo.get('category') === 'vertical') { %> <% if (category === 'vertical') { %>
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a> <span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span>
<% } else { %> <% } else { %>
<span class="wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>"> <span class="wrapper-<%= xblockType %>-title wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="xblock-field-value incontext-editor-value"><%= xblockInfo.get('display_name') %></span> <span class="<%= xblockType %>-title item-title xblock-field-value incontext-editor-value"><%= xblockInfo.get('display_name') %></span>
</span> </span>
<% } %> <% } %>
</h3> </h3>
<div class="item-actions"> <div class="<%= xblockType %>-header-actions">
<ul class="actions-list"> <ul class="actions-list">
<li class="action-item action-delete"> <li class="action-item action-delete">
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button"> <a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
<i class="icon-remove"></i> <i class="icon icon-trash"></i>
<span class="sr"><%= gettext('Delete') %></span> <span class="sr action-button-text"><%= gettext('Delete') %></span>
</a> </a>
</li> </li>
</ul> </ul>
</div>
</div> </div>
<div class="wrapper-xblock-header-secondary"> </div>
<% if (xblockInfo.get('edited_on')) { %> <% if (statusMessage) { %>
<div class="meta-info"> <div class="<%= xblockType %>-status">
<% if (xblockInfo.get('published')) { %> <% if (category !== 'vertical') { %>
<i class="icon-check"></i> <div class="status-release">
<%= gettext('Released:') %> Dec 31, 2015 at 21:00 UTC <p>
<% } else { %> <span class="sr status-release-label">Release Status:</span>
<i class="icon-time"></i> <span class="status-release-value">
<%= gettext('Scheduled:') %> Dec 31, 2015 at 21:00 UTC <% if (xblockInfo.get('released_to_students')) { %>
<% } %> <i class="icon icon-check-sign"></i>
<%= gettext('Released:') %>
<% } else if (xblockInfo.get('release_date')) { %>
<i class="icon icon-time"></i>
<%= gettext('Scheduled:') %>
<% } else { %>
<i class="icon icon-file-alt"></i>
<%= gettext('Unscheduled') %>
<% } %>
<% if (xblockInfo.get('release_date')) { %>
<%= xblockInfo.get('release_date') %>
<% } %>
</span>
</p>
</div> </div>
<% } %> <% } %>
<div class="status-message">
<i class="icon <%= statusIconClass %>"></i>
<div class="item-actions"> <p class="status-message-copy"><%- statusMessage %></p>
<ul class="actions-list">
</ul>
</div> </div>
</div> </div>
</div> <% } %>
<% } %> <% } %>
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %> <% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
<div class="no-content add-xblock-component"> <div class="no-content add-section">
<p><%= gettext("You haven't added any content to this course yet.") %> <p><%= gettext("You haven't added any content to this course yet.") %>
<a href="#" class="add-button" data-category="<%= childCategory %>" <a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"> data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %> <i class="icon icon-plus"></i><%= addChildLabel %>
</a> </a>
</p> </p>
</div> </div>
<% } else { %> <% } else { %>
<ol class="sortable-list sortable-<%= xblockType %>-list"> <div class="outline-content <%= xblockType %>-content">
</ol> <ol class="<%= listType %> is-sortable">
</ol>
<% if (childType) { %> <% if (childType) { %>
<div class="add-xblock-component"> <div class="add-<%= childType %> add-item">
<a href="#" class="add-button" data-category="<%= childCategory %>" <a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"> data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %> <i class="icon icon-plus"></i><%= addChildLabel %>
</a> </a>
</div> </div>
<% } %> <% } %>
</div>
<% } %> <% } %>
<% if (parentInfo) { %> <% if (parentInfo) { %>
......
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
<h3 class="sr">Page Actions</h3> <h3 class="sr">Page Actions</h3>
<ul> <ul>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="toggle-button toggle-button-expand-collapse collapse-all"> <a href="#" class="button button-toggle button-toggle-expand-collapse collapse-all is-hidden">
<span class="collapse-all"><i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span></span> <span class="collapse-all"><i class="icon-arrow-up"></i> <span class="label">Collapse All Sections</span></span>
<span class="expand-all"><i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span></span> <span class="expand-all"><i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span></span>
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href="#" class="button view-button add-button" data-category="chapter" data-parent="mock-course" data-default-name="Section"> <a href="#" class="button button-new" data-category="chapter" data-parent="mock-course" data-default-name="Section">
<i class="icon-plus"></i>New Section <i class="icon-plus"></i>New Section
</a> </a>
</li> </li>
...@@ -33,10 +33,10 @@ ...@@ -33,10 +33,10 @@
<section class="content"> <section class="content">
<article class="content-primary" role="main"> <article class="content-primary" role="main">
<div class="wrapper-dnd"> <div class="wrapper-dnd">
<article class="course-outline" data-locator="mock-course" data-course-key="slashes:MockCourse"> <article class="outline" data-locator="mock-course" data-course-key="slashes:MockCourse">
<div class="no-content add-xblock-component"> <div class="no-content add-xblock-component">
<p>You haven't added any content to this course yet. <p>You haven't added any content to this course yet.
<a href="#" class="add-button" data-category="chapter" data-parent="mock-course" data-default-name="Section"> <a href="#" class="button button-new" data-category="chapter" data-parent="mock-course" data-default-name="Section">
<i class="icon-plus"></i>Add Section <i class="icon-plus"></i>Add Section
</a> </a>
</p> </p>
......
<%
var copy = gettext("Never published");
if (published_on && published_by) {
var message = gettext("Last published %(last_published_date)s by %(publish_username)s");
copy = interpolate(message, {
last_published_date: '<span class="date">' + published_on + '</span>',
publish_username: '<span class="user">' + published_by + '</span>'
}, true);
} else if (published) {
copy = gettext("Previously published");
}
%>
<div class="wrapper-last-publish"> <div class="wrapper-last-publish">
<p class="copy"> <p class="copy"><%= copy %></p>
<% if (published) { </div>
if (published_on && published_by) {
var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %>
<%= interpolate(message, {
last_published_date: '<span class="date">' + published_on + '</span>',
publish_username: '<span class="user">' + published_by + '</span>' }, true) %>
<% } else { %>
<%= gettext("Previously published") %>
<% } %>
<% } else { %>
<%= gettext("Never published") %>
<% } %>
</p>
</div>
\ No newline at end of file
<% <%
var publishClasses = ""; var publishClass = '';
var title = gettext("Draft (Never published)"); if (publishState === 'staff_only') {
if (published) { publishClass = 'is-staff-only';
if (published && hasChanges) { } else if (publishState === 'live') {
publishClasses = publishClasses + " is-draft"; publishClass = 'is-live is-published is-released';
title = gettext("Draft (Unpublished changes)"); } else if (publishState === 'ready') {
} else { publishClass = 'is-ready is-published';
publishClasses = publishClasses + " is-published"; } else if (publishState === 'has_unpublished_content') {
title = gettext("Published"); publishClass = 'has-warnings is-draft';
}
}
if (releaseDate) {
publishClasses = publishClasses + " is-scheduled";
} }
if (visibleToStaffOnly) {
publishClasses = publishClasses + " is-staff-only"; var title = gettext("Draft (Never published)");
if (publishState === 'staff_only') {
title = gettext("Unpublished (Staff only)"); title = gettext("Unpublished (Staff only)");
} else if (publishState === 'live') {
title = gettext("Published and Live");
} else if (publishState === 'ready') {
title = gettext("Published");
} else if (publishState === 'has_unpublished_content') {
title = gettext("Draft (Unpublished changes)");
} }
var releaseLabel = gettext("Release:");
if (publishState === 'live') {
releaseLabel = gettext("Released:");
} else if (publishState === 'ready') {
releaseLabel = gettext("Scheduled:");
}
var canPublish = publishState !== 'ready' && publishState !== 'live';
var canDiscardChanges = publishState === 'has_unpublished_content';
var visibleToStaffOnly = publishState === 'staff_only';
%> %>
<div class="bit-publishing <%= publishClasses %>"> <div class="bit-publishing <%= publishClass %>">
<h3 class="bar-mod-title pub-status"><span class="sr"><%= gettext("Publishing Status") %></span> <h3 class="bar-mod-title pub-status"><span class="sr"><%= gettext("Publishing Status") %></span>
<%= title %> <%= title %>
</h3> </h3>
<div class="wrapper-last-draft bar-mod-content"> <div class="wrapper-last-draft bar-mod-content">
<p class="copy meta"> <p class="copy meta">
<% if (hasChanges && editedOn && editedBy) { <% if (publishState === 'has_unpublished_content' && editedOn && editedBy) {
var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %> var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %>
<%= interpolate(message, { <%= interpolate(message, {
last_saved_date: '<span class="date">' + editedOn + '</span>', last_saved_date: '<span class="date">' + editedOn + '</span>',
...@@ -42,17 +56,7 @@ if (visibleToStaffOnly) { ...@@ -42,17 +56,7 @@ if (visibleToStaffOnly) {
</div> </div>
<div class="wrapper-release bar-mod-content"> <div class="wrapper-release bar-mod-content">
<h5 class="title"> <h5 class="title"><%= releaseLabel %></h5>
<% if (published && releaseDate) {
if (releasedToStudents) { %>
<%= gettext("Released:") %>
<% } else { %>
<%= gettext("Scheduled:") %>
<% }
} else { %>
<%= gettext("Release:") %>
<% } %>
</h5>
<p class="copy"> <p class="copy">
<% if (releaseDate) { %> <% if (releaseDate) { %>
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %> <% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
...@@ -87,12 +91,12 @@ if (visibleToStaffOnly) { ...@@ -87,12 +91,12 @@ if (visibleToStaffOnly) {
<div class="wrapper-pub-actions bar-mod-actions"> <div class="wrapper-pub-actions bar-mod-actions">
<ul class="action-list"> <ul class="action-list">
<li class="action-item"> <li class="action-item">
<a class="action-publish action-primary <% if (published && !hasChanges) { %>is-disabled<% } %>" <a class="action-publish action-primary <% if (!canPublish) { %>is-disabled<% } %>"
href=""><%= gettext("Publish") %> href=""><%= gettext("Publish") %>
</a> </a>
</li> </li>
<li class="action-item"> <li class="action-item">
<a class="action-discard action-secondary <% if (!published || !hasChanges) { %>is-disabled<% } %>" <a class="action-discard action-secondary <% if (!canDiscardChanges) { %>is-disabled<% } %>"
href=""><%= gettext("Discard Changes") %> href=""><%= gettext("Discard Changes") %>
</a> </a>
</li> </li>
......
<%
var publishState = xblockInfo.get('publish_state');
var publishClass = '';
if (publishState === 'staff_only') {
publishClass = 'is-staff-only';
} else if (publishState === 'live') {
publishClass = 'is-live';
} else if (publishState === 'ready') {
publishClass = 'is-ready';
} else if (publishState === 'has_unpublished_content') {
publishClass = 'has_warnings';
}
var listType = 'list-for-' + xblockType;
if (xblockType === 'course') {
listType = 'list-sections';
} else if (xblockType === 'section') {
listType = 'list-subsections';
} else if (xblockType === 'subsection') {
listType = 'list-units';
}
%>
<% if (parentInfo) { %> <% if (parentInfo) { %>
<li class="outline-item outline-item-<%= xblockType %>" <li class="outline-item outline-<%= xblockType %> <%= publishClass %>"
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>"> data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
<div class="wrapper-xblock-header"> <div class="<%= xblockType %>-header">
<div class="wrapper-xblock-header-primary"> <h3 class="<%= xblockType %>-header-details">
<h3 class="xblock-title"> <span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a> <a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span>
</h3> </h3>
</div> </div>
</div>
<% } %> <% } %>
<ol class="sortable-list sortable-<%= xblockType %>-list"> <div class="<%= xblockType %>-content outline-content">
</ol> <ol class="<%= listType %>">
</ol>
<% if (childType) { %> <% if (childType) { %>
<div class="add-xblock-component"> <div class="add-<%= childType %> add-item">
<a href="#" class="add-button" data-category="<%= childCategory %>" <a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"> data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %> <i class="icon icon-plus"></i><%= addChildLabel %>
</a> </a>
</div> </div>
<% } %> <% } %>
</div>
<% if (parentInfo) { %> <% if (parentInfo) { %>
</li> </li>
<% } %> <% } %>
<% if (parentInfo) { %> <% if (parentInfo) { %>
<li class="outline-item outline-item-<%= xblockType %> <%= includesChildren ? 'is-collapsible' : '' %> is-draggable <%= isCollapsed ? 'collapsed' : '' %>" <li class="outline-item outline-item-<%= xblockType %> <%= includesChildren ? 'is-collapsible' : '' %> is-draggable <%= isCollapsed ? 'is-collapsed' : '' %>"
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>"> data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span> <span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span>
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %> <% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
<div class="no-content add-xblock-component"> <div class="no-content add-xblock-component">
<p><%= gettext("You haven't added any content to this course yet.") %> <p><%= gettext("You haven't added any content to this course yet.") %>
<a href="#" class="add-button" data-category="<%= childCategory %>" <a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"> data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %> <i class="icon-plus"></i><%= addChildLabel %>
</a> </a>
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
<% if (childType) { %> <% if (childType) { %>
<div class="add-xblock-component"> <div class="add-xblock-component">
<a href="#" class="add-button" data-category="<%= childCategory %>" <a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"> data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %> <i class="icon-plus"></i><%= addChildLabel %>
</a> </a>
......
...@@ -89,11 +89,11 @@ class ModuleStoreEnum(object): ...@@ -89,11 +89,11 @@ class ModuleStoreEnum(object):
# user ID to use for tests that do not have a django user available # user ID to use for tests that do not have a django user available
test = -3 test = -3
class PublishState(object):
"""
The publish state for a given xblock-- either 'draft', 'private', or 'public'.
Currently in CMS, an xblock can only be in 'draft' or 'private' if it is at or below the Unit level. class LegacyPublishState(object):
"""
The legacy publish state for a given xblock-- either 'draft', 'private', or 'public'. These states
are no longer used in Studio directly, but are still referenced in a few places.
""" """
draft = 'draft' draft = 'draft'
private = 'private' private = 'private'
...@@ -301,10 +301,10 @@ class ModuleStoreRead(object): ...@@ -301,10 +301,10 @@ class ModuleStoreRead(object):
Returns whether this xblock is draft, public, or private. Returns whether this xblock is draft, public, or private.
Returns: Returns:
PublishState.draft - content is in the process of being edited, but still has a previous LegacyPublishState.draft - content is in the process of being edited, but still has a previous
version deployed to LMS version deployed to LMS
PublishState.public - content is locked and deployed to LMS LegacyPublishState.public - content is locked and deployed to LMS
PublishState.private - content is editable and not deployed to LMS LegacyPublishState.private - content is editable and not deployed to LMS
""" """
pass pass
...@@ -522,7 +522,7 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -522,7 +522,7 @@ class ModuleStoreReadBase(ModuleStoreRead):
""" """
Returns PublishState.public since this is a read-only store. Returns PublishState.public since this is a read-only store.
""" """
return PublishState.public return LegacyPublishState.public
def heartbeat(self): def heartbeat(self):
""" """
......
...@@ -439,10 +439,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -439,10 +439,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Returns whether this xblock is draft, public, or private. Returns whether this xblock is draft, public, or private.
Returns: Returns:
PublishState.draft - content is in the process of being edited, but still has a previous LegacyPublishState.draft - content is in the process of being edited, but still has a previous
version deployed to LMS version deployed to LMS
PublishState.public - content is locked and deployed to LMS LegacyPublishState.public - content is locked and deployed to LMS
PublishState.private - content is editable and not deployed to LMS LegacyPublishState.private - content is editable and not deployed to LMS
""" """
course_id = xblock.scope_ids.usage_id.course_key course_id = xblock.scope_ids.usage_id.course_key
store = self._get_modulestore_for_courseid(course_id) store = self._get_modulestore_for_courseid(course_id)
......
...@@ -11,7 +11,7 @@ import logging ...@@ -11,7 +11,7 @@ import logging
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.exceptions import InvalidVersionError from xmodule.exceptions import InvalidVersionError
from xmodule.modulestore import PublishState, ModuleStoreEnum from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum
from xmodule.modulestore.exceptions import ( from xmodule.modulestore.exceptions import (
ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError
) )
...@@ -613,7 +613,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -613,7 +613,7 @@ class DraftModuleStore(MongoModuleStore):
return False return False
# don't check children if this block has changes (is not public) # don't check children if this block has changes (is not public)
if self.compute_publish_state(item) != PublishState.public: if self.compute_publish_state(item) != LegacyPublishState.public:
return True return True
# if this block doesn't have changes, then check its children # if this block doesn't have changes, then check its children
elif item.has_children: elif item.has_children:
...@@ -792,10 +792,10 @@ class DraftModuleStore(MongoModuleStore): ...@@ -792,10 +792,10 @@ class DraftModuleStore(MongoModuleStore):
Returns whether this xblock is draft, public, or private. Returns whether this xblock is draft, public, or private.
Returns: Returns:
PublishState.draft - content is in the process of being edited, but still has a previous LegacyPublishState.draft - content is in the process of being edited, but still has a previous
version deployed to LMS version deployed to LMS
PublishState.public - content is locked and deployed to LMS LegacyPublishState.public - content is locked and deployed to LMS
PublishState.private - content is editable and not deployed to LMS LegacyPublishState.private - content is editable and not deployed to LMS
""" """
if getattr(xblock, 'is_draft', False): if getattr(xblock, 'is_draft', False):
published_xblock_location = as_published(xblock.location) published_xblock_location = as_published(xblock.location)
...@@ -803,11 +803,11 @@ class DraftModuleStore(MongoModuleStore): ...@@ -803,11 +803,11 @@ class DraftModuleStore(MongoModuleStore):
{'_id': published_xblock_location.to_deprecated_son()} {'_id': published_xblock_location.to_deprecated_son()}
) )
if published_item is None: if published_item is None:
return PublishState.private return LegacyPublishState.private
else: else:
return PublishState.draft return LegacyPublishState.draft
else: else:
return PublishState.public return LegacyPublishState.public
def _verify_branch_setting(self, expected_branch_setting): def _verify_branch_setting(self, expected_branch_setting):
""" """
......
...@@ -4,7 +4,7 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore ...@@ -4,7 +4,7 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
from split import SplitMongoModuleStore, EXCLUDE_ALL from split import SplitMongoModuleStore, EXCLUDE_ALL
from xmodule.modulestore import ModuleStoreEnum, PublishState from xmodule.modulestore import ModuleStoreEnum, LegacyPublishState
from xmodule.modulestore.exceptions import InsufficientSpecificationError from xmodule.modulestore.exceptions import InsufficientSpecificationError
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
...@@ -251,10 +251,11 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -251,10 +251,11 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Returns whether this xblock is draft, public, or private. Returns whether this xblock is draft, public, or private.
Returns: Returns:
PublishState.draft - published exists and is different from draft LegacyPublishState.draft - published exists and is different from draft
PublishState.public - published exists and is the same as draft LegacyPublishState.public - published exists and is the same as draft
PublishState.private - no published version exists LegacyPublishState.private - no published version exists
""" """
# TODO figure out what to say if xblock is not from the HEAD of its branch
def get_head(branch): def get_head(branch):
course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure'] course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure']
return self._get_block_from_structure(course_structure, xblock.location.block_id) return self._get_block_from_structure(course_structure, xblock.location.block_id)
...@@ -271,13 +272,13 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -271,13 +272,13 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
if not published_head: if not published_head:
# published version does not exist # published version does not exist
return PublishState.private return LegacyPublishState.private
elif get_version(draft_head) == get_version(published_head): elif get_version(draft_head) == get_version(published_head):
# published and draft versions are equal # published and draft versions are equal
return PublishState.public return LegacyPublishState.public
else: else:
# published and draft versions differ # published and draft versions differ
return PublishState.draft return LegacyPublishState.draft
def convert_to_draft(self, location, user_id): def convert_to_draft(self, location, user_id):
""" """
......
...@@ -9,7 +9,7 @@ from pytz import UTC ...@@ -9,7 +9,7 @@ from pytz import UTC
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum, PublishState from xmodule.modulestore import ModuleStoreEnum, LegacyPublishState
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.exceptions import InvalidVersionError from xmodule.exceptions import InvalidVersionError
...@@ -991,22 +991,22 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -991,22 +991,22 @@ class TestMixedModuleStore(unittest.TestCase):
item_location = item.location.version_agnostic() item_location = item.location.version_agnostic()
mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID)) mongo_store = self.store._get_modulestore_for_courseid(self._course_key_from_string(self.MONGO_COURSEID))
with check_mongo_calls(mongo_store, max_find, max_send): with check_mongo_calls(mongo_store, max_find, max_send):
self.assertEquals(self.store.compute_publish_state(item), PublishState.private) self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.private)
# Private -> Public # Private -> Public
self.store.publish(item_location, self.user_id) self.store.publish(item_location, self.user_id)
item = self.store.get_item(item_location) item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.public) self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.public)
# Public -> Private # Public -> Private
self.store.unpublish(item_location, self.user_id) self.store.unpublish(item_location, self.user_id)
item = self.store.get_item(item_location) item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.private) self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.private)
# Private -> Public # Private -> Public
self.store.publish(item_location, self.user_id) self.store.publish(item_location, self.user_id)
item = self.store.get_item(item_location) item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.public) self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.public)
# Public -> Draft with NO changes # Public -> Draft with NO changes
# Note: This is where Split and Mongo differ # Note: This is where Split and Mongo differ
...@@ -1014,14 +1014,14 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1014,14 +1014,14 @@ class TestMixedModuleStore(unittest.TestCase):
item = self.store.get_item(item_location) item = self.store.get_item(item_location)
self.assertEquals( self.assertEquals(
self.store.compute_publish_state(item), self.store.compute_publish_state(item),
PublishState.draft if default_ms == 'draft' else PublishState.public LegacyPublishState.draft if default_ms == 'draft' else LegacyPublishState.public
) )
# Draft WITH changes # Draft WITH changes
item.display_name = 'new name' item.display_name = 'new name'
item = self.store.update_item(item, self.user_id) item = self.store.update_item(item, self.user_id)
self.assertTrue(self.store.has_changes(item.location)) self.assertTrue(self.store.has_changes(item.location))
self.assertEquals(self.store.compute_publish_state(item), PublishState.draft) self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.draft)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_auto_publish(self, default_ms): def test_auto_publish(self, default_ms):
......
...@@ -113,7 +113,7 @@ class CourseOutlineContainer(CourseOutlineItem): ...@@ -113,7 +113,7 @@ class CourseOutlineContainer(CourseOutlineItem):
""" """
click_css( click_css(
self, self,
self._bounded_selector(".add-xblock-component a.add-button"), self._bounded_selector(".add-item a.button-new"),
require_notification=require_notification, require_notification=require_notification,
) )
...@@ -125,7 +125,7 @@ class CourseOutlineContainer(CourseOutlineItem): ...@@ -125,7 +125,7 @@ class CourseOutlineContainer(CourseOutlineItem):
self.browser.execute_script("jQuery.fx.off = true;") self.browser.execute_script("jQuery.fx.off = true;")
def subsection_expanded(): def subsection_expanded():
add_button = self.q(css=self._bounded_selector('> .add-xblock-component a.add-button')).first.results add_button = self.q(css=self._bounded_selector('> .outline-content > .add-item a.button-new')).first.results
return add_button and add_button[0].is_displayed() return add_button and add_button[0].is_displayed()
currently_expanded = subsection_expanded() currently_expanded = subsection_expanded()
...@@ -171,8 +171,8 @@ class CourseOutlineUnit(CourseOutlineChild): ...@@ -171,8 +171,8 @@ class CourseOutlineUnit(CourseOutlineChild):
PageObject that wraps a unit link on the Studio Course Outline page. PageObject that wraps a unit link on the Studio Course Outline page.
""" """
url = None url = None
BODY_SELECTOR = '.outline-item-unit' BODY_SELECTOR = '.outline-unit'
NAME_SELECTOR = '.xblock-title a' NAME_SELECTOR = '.unit-title a'
def go_to(self): def go_to(self):
""" """
...@@ -191,7 +191,9 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer): ...@@ -191,7 +191,9 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
""" """
url = None url = None
BODY_SELECTOR = '.outline-item-subsection' BODY_SELECTOR = '.outline-subsection'
NAME_SELECTOR = '.subsection-title'
NAME_FIELD_WRAPPER_SELECTOR = '.subsection-header .wrapper-xblock-field'
CHILD_CLASS = CourseOutlineUnit CHILD_CLASS = CourseOutlineUnit
def unit(self, title): def unit(self, title):
...@@ -224,7 +226,9 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer): ...@@ -224,7 +226,9 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer):
:class`.PageObject` that wraps a section block on the Studio Course Outline page. :class`.PageObject` that wraps a section block on the Studio Course Outline page.
""" """
url = None url = None
BODY_SELECTOR = '.outline-item-section' BODY_SELECTOR = '.outline-section'
NAME_SELECTOR = '.section-title'
NAME_FIELD_WRAPPER_SELECTOR = '.section-header .wrapper-xblock-field'
CHILD_CLASS = CourseOutlineSubsection CHILD_CLASS = CourseOutlineSubsection
def subsection(self, title): def subsection(self, title):
...@@ -268,7 +272,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -268,7 +272,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
url_path = "course" url_path = "course"
CHILD_CLASS = CourseOutlineSection CHILD_CLASS = CourseOutlineSection
EXPAND_COLLAPSE_CSS = '.toggle-button-expand-collapse' EXPAND_COLLAPSE_CSS = '.toggle-button-expand-collapse'
BOTTOM_ADD_SECTION_BUTTON = '.course-outline > .add-xblock-component .add-button' BOTTOM_ADD_SECTION_BUTTON = '.outline > .add-section .button-new'
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='body.view-outline').present return self.q(css='body.view-outline').present
...@@ -337,7 +341,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -337,7 +341,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
""" """
Clicks the button for adding a section which resides at the top of the screen. Clicks the button for adding a section which resides at the top of the screen.
""" """
click_css(self, '.wrapper-mast nav.nav-actions .add-button') click_css(self, '.wrapper-mast nav.nav-actions .button-new')
def add_section_from_bottom_button(self): def add_section_from_bottom_button(self):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment