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():
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)
......
......@@ -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 > .wrapper-xblock-header a.delete-button'
world.css_click(delete_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)
......
......@@ -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')
......@@ -1209,7 +1209,7 @@ class ContentStoreTest(ContentStoreTestCase):
resp = self._show_course_overview(course.id)
self.assertContains(
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',
course_key='MITx/999/Robot_Super_Course',
),
......
......@@ -9,7 +9,7 @@ from django.test.client import Client
from django.contrib.auth.models import User
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.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
......@@ -151,16 +151,16 @@ class CourseTestCase(ModuleStoreTestCase):
# create a Draft vertical
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)
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
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
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)
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
sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL))
......@@ -197,7 +197,7 @@ class CourseTestCase(ModuleStoreTestCase):
def verify_item_publish_state(item, publish_state):
"""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))
else:
self.assertFalse(getattr(item, 'is_draft', False))
......@@ -210,18 +210,18 @@ class CourseTestCase(ModuleStoreTestCase):
return item
# 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():
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
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
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
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
for vert in [vertical, private_vertical, public_vertical]:
......@@ -332,7 +332,7 @@ class CourseTestCase(ModuleStoreTestCase):
it'll return public in that case
"""
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
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():
......@@ -345,13 +345,13 @@ class CourseTestCase(ModuleStoreTestCase):
# checking children: if published differs from item, return draft
return supposed_state
# published == item in all respects, so return public
return PublishState.public
elif supposed_state == PublishState.public and item.location.category in DIRECT_ONLY_CATEGORIES:
return LegacyPublishState.public
elif supposed_state == LegacyPublishState.public and item.location.category in DIRECT_ONLY_CATEGORIES:
if not all([
self.store.has_item(child_loc, revision=ModuleStoreEnum.RevisionOption.draft_only)
for child_loc in item.children
]):
return PublishState.draft
return LegacyPublishState.draft
else:
return supposed_state
else:
......
......@@ -155,10 +155,10 @@ 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
LegacyPublishState.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
LegacyPublishState.public - content is locked and deployed to LMS
LegacyPublishState.private - content is editable and not deployed to LMS
"""
return modulestore().compute_publish_state(xblock)
......
......@@ -12,7 +12,7 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from edxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import PublishState
from xmodule.modulestore import LegacyPublishState
from xblock.core import XBlock
from xblock.django.request import webob_to_django_response, django_to_webob_request
......@@ -101,7 +101,7 @@ def subsection_handler(request, usage_key_string):
subsection_units = item.get_children()
for unit in subsection_units:
state = compute_publish_state(unit)
if state in (PublishState.public, PublishState.draft):
if state in (LegacyPublishState.public, LegacyPublishState.draft):
can_view_live = True
break
......
......@@ -404,7 +404,7 @@ def course_outline_initial_state(locator_to_show, course_structure):
"""
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)
......@@ -417,7 +417,7 @@ def course_outline_initial_state(locator_to_show, course_structure):
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)
......
......@@ -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
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):
"""
......@@ -623,11 +619,23 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
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 = {
"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),
......@@ -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,
"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,
"publish_state": _compute_publish_state(xblock, child_info) if not xblock.category == 'course' else None
}
if data is not None:
xblock_info["data"] = data
......@@ -646,13 +654,70 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
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
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):
"""
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
from contentstore.views.course import course_outline_initial_state
from course_action_state.models import CourseRerunState
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.tests.factories import CourseFactory, ItemFactory
from opaque_keys.edx.locator import CourseLocator
......@@ -229,7 +230,7 @@ class TestCourseOutline(CourseTestCase):
self.assertEqual(json_response['category'], 'course')
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['publish_state'])
# Now verify the first child
children = json_response['child_info']['children']
......@@ -238,7 +239,7 @@ class TestCourseOutline(CourseTestCase):
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.assertEqual(first_child_response['publish_state'], PublishState.unscheduled)
self.assertTrue(len(first_child_response['child_info']['children']) > 0)
# Finally, validate the entire response for consistency
......@@ -251,7 +252,6 @@ class TestCourseOutline(CourseTestCase):
self.assertIsNotNone(json_response['display_name'])
self.assertIsNotNone(json_response['id'])
self.assertIsNotNone(json_response['category'])
self.assertIsNotNone(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)
......
"""Tests for items views."""
import json
from datetime import datetime
from datetime import datetime, timedelta
import ddt
from mock import patch
......@@ -19,12 +19,11 @@ from contentstore.views.component import (
component_handler, get_component_templates
)
from contentstore.views.item import create_xblock_info, ALWAYS
from contentstore.views.item import create_xblock_info, ALWAYS, PublishState
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 import LegacyPublishState, ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.x_module import STUDIO_VIEW, STUDENT_VIEW
......@@ -433,7 +432,8 @@ class TestEditItem(ItemTest):
"""
item = self.get_item_from_modulestore(
usage_key,
(expected_publish_state == PublishState.private) or (expected_publish_state == PublishState.draft)
(expected_publish_state == LegacyPublishState.private) or
(expected_publish_state == LegacyPublishState.draft)
)
self.assertEqual(expected_publish_state, self.store.compute_publish_state(item))
return item
......@@ -546,12 +546,12 @@ class TestEditItem(ItemTest):
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.verify_publish_state(self.problem_usage_key, LegacyPublishState.private)
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public)
def test_make_draft(self):
""" Test creating a draft version of a public problem. """
......@@ -564,7 +564,7 @@ class TestEditItem(ItemTest):
self.problem_update_url,
data={'publish': 'discard_changes'}
)
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public)
self.assertIsNone(published.due)
def test_republish(self):
......@@ -576,7 +576,7 @@ class TestEditItem(ItemTest):
}
# 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.verify_publish_state(self.problem_usage_key, LegacyPublishState.private)
# Republishing when only in draft will update the draft but not cause a public item to be created.
self.client.ajax_post(
......@@ -588,7 +588,7 @@ class TestEditItem(ItemTest):
}
}
)
self.verify_publish_state(self.problem_usage_key, PublishState.private)
self.verify_publish_state(self.problem_usage_key, LegacyPublishState.private)
draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
self.assertEqual(draft.display_name, new_display_name)
......@@ -609,7 +609,7 @@ class TestEditItem(ItemTest):
}
}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public)
published = modulestore().get_item(
self.problem_usage_key,
revision=ModuleStoreEnum.RevisionOption.published_only
......@@ -625,7 +625,7 @@ class TestEditItem(ItemTest):
self.problem_update_url,
data={'publish': 'make_public'}
)
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public)
# Update the draft version and check that published is different.
self.client.ajax_post(
......@@ -659,7 +659,7 @@ class TestEditItem(ItemTest):
self.problem_update_url,
data={'publish': 'make_public'}
)
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
published = self.verify_publish_state(self.problem_usage_key, LegacyPublishState.public)
# Now make a draft
self.client.ajax_post(
......@@ -704,8 +704,8 @@ class TestEditItem(ItemTest):
# 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.verify_publish_state(unit_usage_key, LegacyPublishState.private)
self.verify_publish_state(html_usage_key, LegacyPublishState.private)
# Make the unit public and verify that the problem is also made public
resp = self.client.ajax_post(
......@@ -713,8 +713,8 @@ class TestEditItem(ItemTest):
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_publish_state(unit_usage_key, LegacyPublishState.public)
self.verify_publish_state(html_usage_key, LegacyPublishState.public)
# Make a draft for the unit and verify that the problem also has a draft
resp = self.client.ajax_post(
......@@ -725,8 +725,8 @@ class TestEditItem(ItemTest):
}
)
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_publish_state(unit_usage_key, LegacyPublishState.draft)
self.verify_publish_state(html_usage_key, LegacyPublishState.draft)
class TestEditSplitModule(ItemTest):
......@@ -1148,7 +1148,6 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['category'], 'course')
self.assertEqual(xblock_info['id'], 'i4x://MITx/999/course/Robot_Super_Course')
self.assertEqual(xblock_info['display_name'], 'Robot Super Course')
self.assertTrue(xblock_info['published'])
# Finally, validate the entire response for consistency
self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info)
......@@ -1160,7 +1159,6 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['category'], 'chapter')
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
......@@ -1173,7 +1171,6 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['category'], 'sequential')
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
......@@ -1186,7 +1183,6 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['category'], 'vertical')
self.assertEqual(xblock_info['id'], 'i4x://MITx/999/vertical/Unit_1')
self.assertEqual(xblock_info['display_name'], 'Unit 1')
self.assertTrue(xblock_info['published'])
self.assertEqual(xblock_info['edited_by'], 'testuser')
# Validate that the correct ancestor info has been included
......@@ -1208,7 +1204,6 @@ class TestXBlockInfo(ItemTest):
self.assertEqual(xblock_info['category'], 'video')
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
......@@ -1221,7 +1216,6 @@ class TestXBlockInfo(ItemTest):
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')
if has_ancestor_info:
self.assertIsNotNone(xblock_info.get('ancestor_info', None))
......@@ -1243,3 +1237,141 @@ class TestXBlockInfo(ItemTest):
)
else:
self.assertIsNone(xblock_info.get('child_info', None))
class TestXBlockPublishingInfo(ItemTest):
"""
Unit tests for XBlock's outline handling.
"""
def _create_child(self, parent, category, display_name, publish_item=False):
return ItemFactory.create(
parent_location=parent.location, category=category, display_name=display_name,
user_id=self.user.id, publish_item=publish_item
)
def _get_child(self, xblock_info, index):
"""
Returns the child 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 test_empty_chapter_publishing_info(self):
empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter")
xblock_info = self._get_xblock_info(empty_chapter.location)
self.assertEqual(xblock_info['publish_state'], PublishState.unscheduled)
def test_empty_section_publishing_info(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.assertEqual(xblock_info['publish_state'], PublishState.unscheduled)
self.assertEqual(self._get_child(xblock_info, 0)['publish_state'], PublishState.unscheduled)
def test_published_unit_publishing_info(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._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1))
xblock_info = self._get_xblock_info(chapter.location)
self.assertEqual(xblock_info['publish_state'], PublishState.ready)
sequential_child_info = self._get_child(xblock_info, 0)
self.assertEqual(sequential_child_info['publish_state'], PublishState.ready)
unit_child_info = self._get_child(sequential_child_info, 0)
self.assertEqual(unit_child_info['publish_state'], PublishState.ready)
def test_released_unit_publishing_info(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._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1))
xblock_info = self._get_xblock_info(chapter.location)
self.assertEqual(xblock_info['publish_state'], PublishState.live)
sequential_child_info = self._get_child(xblock_info, 0)
self.assertEqual(sequential_child_info['publish_state'], PublishState.live)
unit_child_info = self._get_child(sequential_child_info, 0)
self.assertEqual(unit_child_info['publish_state'], PublishState.live)
def test_partially_released_section_publishing_info(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._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._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
released_sequential_child_info = self._get_child(xblock_info, 0)
released_unit_child_info = self._get_child(released_sequential_child_info, 0)
self.assertEqual(released_unit_child_info['publish_state'], PublishState.live)
self.assertEqual(released_sequential_child_info['publish_state'], PublishState.live)
# Verify the state of the published sequential
public_sequential_child_info = self._get_child(xblock_info, 1)
public_unit_child_info = self._get_child(public_sequential_child_info, 0)
self.assertEqual(public_sequential_child_info['publish_state'], PublishState.ready)
self.assertEqual(public_unit_child_info['publish_state'], PublishState.ready)
# Finally verify the state of the chapter
self.assertEqual(xblock_info['publish_state'], PublishState.ready)
def test_unpublished_changes_publishing_info(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", publish_item=True)
self._set_display_name(unit.location, 'Updated Unit')
xblock_info = self._get_xblock_info(chapter.location)
self.assertEqual(xblock_info['publish_state'], PublishState.has_unpublished_content)
sequential_child_info = self._get_child(xblock_info, 0)
self.assertEqual(sequential_child_info['publish_state'], PublishState.has_unpublished_content)
unit_child_info = self._get_child(sequential_child_info, 0)
self.assertEqual(unit_child_info['publish_state'], PublishState.has_unpublished_content)
def test_staff_only_publishing_info(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.assertEqual(xblock_info['publish_state'], PublishState.staff_only)
sequential_child_info = self._get_child(xblock_info, 0)
self.assertEqual(sequential_child_info['publish_state'], PublishState.staff_only)
unit_child_info = self._get_child(sequential_child_info, 0)
self.assertEqual(unit_child_info['publish_state'], PublishState.staff_only)
......@@ -25,21 +25,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"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.
*/
"edited_on":null,
......@@ -48,6 +33,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"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.
*/
"published_on": null,
......@@ -56,6 +45,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"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.
*/
"released_to_students": null,
......
......@@ -23,11 +23,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
published: false,
has_changes: false,
publish_state: '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 +77,30 @@ 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({publish_state: 'ready'});
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
fetch({"published": false});
fetch({publish_state: 'unscheduled'});
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
});
it('updates when has_changes attribute changes', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"has_changes": true});
fetch({publish_state: 'has-unpublished-changes'});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
fetch({"published": true, "has_changes": false});
fetch({publish_state: 'ready'});
expect(containerPage.$(previewCss)).toHaveClass(disabledCss);
// If published is false, preview is always enabled.
fetch({"published": false, "has_changes": false});
fetch({publish_state: 'unscheduled'});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
});
});
......@@ -111,9 +108,10 @@ 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",
hasWarningsClass = 'has-warnings',
publishButtonCss = ".action-publish",
discardChangesButtonCss = ".action-discard",
lastDraftCss = ".wrapper-last-draft",
......@@ -125,9 +123,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
// Helper function to do the discard operation, up until the server response.
containerPage.render();
respondWithHtml(mockContainerXBlockHtml);
fetch({"published": true, "has_changes": true});
fetch({publish_state: 'has_unpublished_content'});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
// Click discard changes
containerPage.$(discardChangesButtonCss).click();
......@@ -145,43 +143,43 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
promptSpies.show.andReturn(this.promptSpies);
});
it('renders correctly with private content', function () {
it('renders correctly with unscheduled content', function () {
var verifyPrivateState = function() {
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);
};
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false, "has_changes": false});
verifyPrivateState();
fetch({"published": false, "has_changes": true});
fetch({publishState: 'unscheduled'});
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({publish_state: 'ready'});
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);
fetch({"published": true, "has_changes": true});
fetch({publish_state: 'has_unpublished_content'});
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);
fetch({publish_state: 'live'});
expect(containerPage.$(headerCss).text()).toContain('Published');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass);
});
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(readyClass);
// Click publish
containerPage.$(publishButtonCss).click();
......@@ -197,18 +195,17 @@ 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({publish_state: 'ready'}));
// Verify updates displayed
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
// Verify that the "publish_state" value has been updated
expect(containerPage.model.get("publish_state")).toBe('ready');
});
it('can does not fetch if publish fails', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false});
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
// Click publish
containerPage.$(publishButtonCss).click();
......@@ -220,9 +217,9 @@ 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 that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
// Verify that the "publish_state" value has been updated
expect(containerPage.model.get("publish_state")).toBe('unscheduled');
});
it('can discard changes', function () {
......@@ -263,11 +260,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});
renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'has_unpublished_content' });
var numRequests = requests.length;
// Click discard changes
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
containerPage.$(discardChangesButtonCss).click();
// Click cancel to confirmation.
......@@ -280,14 +277,17 @@ 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({
publish_state: 'has_unpublished_content',
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 +295,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({
publish_state: '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 +306,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({
publish_state: '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 +317,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({
publish_state: '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({
publish_state: 'has_unpublished_content', 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()).
......@@ -355,8 +362,7 @@ 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
publish_state: isStaffOnly ? 'staff_only' : 'unscheduled'
}));
};
......@@ -364,11 +370,11 @@ 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);
} 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);
}
};
......@@ -386,27 +392,16 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it("can remove staff only setting", function() {
promptSpy = edit_helpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml);
requestStaffOnly(true);
renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'staff_only' });
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);
requestStaffOnly(false);
verifyStaffOnly(false);
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
});
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, { publish_state: 'staff_only' });
requestCount = requests.length;
containerPage.$('.action-staff-lock').click();
edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
......@@ -429,25 +424,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");
});
......
......@@ -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,10 @@ 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,
publish_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
])
}])
])
]);
mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []);
......@@ -129,9 +137,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 +150,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 +172,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 +192,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 +200,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 +212,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 +230,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 +246,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 +254,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 +263,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 +276,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 +336,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 +346,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 +365,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 +394,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 +414,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,12 +425,16 @@ 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');
});
});
describe("Publishing State", function() {
// TODO: implement this!!!!
});
});
});
......@@ -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',
publish_state: 'unscheduled',
ancestor_info: {
ancestors: [{
id: 'mock-subsection',
category: 'sequential',
display_name: 'Mock Subsection',
studio_url: '/course/mock-course?show=mock-subsection',
publish_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',
publish_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',
publish_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',
publish_state: 'unscheduled'
}, {
id: 'mock-course',
category: 'course',
display_name: 'Mock Course',
studio_url: '/course/mock-course'
studio_url: '/course/mock-course',
publish_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);
});
});
});
......@@ -16,6 +16,10 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
"click .ui-toggle-expansion": "toggleExpandCollapse"
},
options: {
collapsedClass: 'collapsed'
},
//override the constructor function
constructor: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
......@@ -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.
event.stopPropagation();
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
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
options: {
collapsedClass: 'is-collapsed'
},
view: 'container_preview',
initialize: function(options) {
......
......@@ -7,7 +7,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
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.
*/
var ContainerStateListenerView = BaseView.extend({
......@@ -53,19 +53,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
*/
var PreviewActionController = ContainerStateListenerView.extend({
shouldRefresh: function(model) {
return ViewUtils.hasChangedAttributes(model, ['has_changes', 'published']);
return ViewUtils.hasChangedAttributes(model, ['edited_on', 'published_on', 'publish_state']);
},
render: function() {
var previewAction = this.$el.find('.button-preview'),
viewLiveAction = this.$el.find('.button-view');
if (this.model.get('published')) {
viewLiveAction = this.$el.find('.button-view'),
publishState = this.model.get('publish_state');
if (publishState !== 'unscheduled') {
viewLiveAction.removeClass(disabledCss);
}
else {
viewLiveAction.addClass(disabledCss);
}
if (this.model.get('has_changes') || !this.model.get('published')) {
if (publishState !== 'live' && publishState !== 'ready') {
previewAction.removeClass(disabledCss);
}
else {
......@@ -98,25 +99,22 @@ 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'
])) {
if (ViewUtils.hasChangedAttributes(model, [ 'edited_on', 'published_on', 'publish_state' ])) {
this.render();
}
},
render: function () {
this.$el.html(this.template({
hasChanges: this.model.get('has_changes'),
published: this.model.get('published'),
publishState: this.model.get('publish_state'),
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 +136,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 +162,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('publish_state') !== 'staff_only';
revertCheckBox = function() {
self.checkStaffLock(!enableStaffLock);
......@@ -223,7 +221,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, ['published', 'published_on', 'published_by'])) {
if (ViewUtils.hasChangedAttributes(model, ['published_on'])) {
this.render();
}
},
......
......@@ -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');
}
});
}
......
......@@ -10,6 +10,7 @@ define(['js/views/xblock_outline'],
// takes XBlockInfo as a model
templateName: 'unit-outline',
className: 'group-configurations-list',
render: function() {
XBlockOutlineView.prototype.render.call(this);
......@@ -23,7 +24,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 +34,7 @@ define(['js/views/xblock_outline'],
ancestorView.render();
listElement.append(ancestorView.$el);
previousAncestor = ancestor;
listElement = ancestorView.$('.sortable-list');
listElement = ancestorView.getListElement();
}
}
return ancestorView;
......
......@@ -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();
};
......
......@@ -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() {
......@@ -94,8 +98,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 +144,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 +171,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;
......
......@@ -318,7 +318,7 @@ $outline-indent-width: $baseline;
border-left-color: $color-live;
}
// CASE: has staff-only content
// CASE: is presented for staff only
&.is-staff-only {
border-left-color: $color-staff-only;
}
......
......@@ -25,14 +25,15 @@
font-weight: 600;
}
// TODO: abstract out
.is-editable {
.incontext-editor-input {
@extend %t-title4;
background: none repeat scroll 0 0 white;
@extend %t-strong;
background: none repeat scroll 0 0 $white;
border: 0;
box-shadow: 0 0 2px 2px $shadow inset;
font-weight: 600;
}
}
}
......
......@@ -6,6 +6,37 @@
%outline-item-header {
@include clearfix();
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 {
......
......@@ -10,7 +10,6 @@ else:
<%!
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 _
%>
......
......@@ -47,13 +47,13 @@ from contentstore.utils import reverse_usage_url
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
<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="expand-all"><i class="icon-arrow-down"></i> <span class="label">${_("Expand All Sections")}</span></span>
</a>
</li>
<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')}
</a>
</li>
......@@ -72,7 +72,7 @@ from contentstore.utils import reverse_usage_url
<%
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>
</div>
<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) { %>
<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') %>">
<div class="wrapper-xblock-header">
<div class="wrapper-xblock-header-primary">
<% if (includesChildren) { %>
<h3 class="xblock-title expand-collapse <%= isCollapsed ? 'expand' : 'collapse' %>" title="<%= gettext('Collapse/Expand this Checklist') %>">
<i class="icon-caret-down ui-toggle-expansion"></i>
<% } else { %>
<h3 class="xblock-title">
<% } %>
<div class="<%= xblockType %>-header">
<% if (includesChildren) { %>
<h3 class="<%= xblockType %>-header-details expand-collapse <%= isCollapsed ? 'expand' : 'collapse' %> ui-toggle-expansion" title="<%= gettext('Collapse/Expand this Checklist') %>">
<i class="icon-caret-down icon"></i>
<% } else { %>
<h3 class="<%= xblockType %>-header-details">
<% } %>
<% if (xblockInfo.get('category') === 'vertical') { %>
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
<% if (category === 'vertical') { %>
<span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span>
<% } else { %>
<span class="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="wrapper-<%= xblockType %>-title wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="<%= xblockType %>-title item-title xblock-field-value incontext-editor-value"><%= xblockInfo.get('display_name') %></span>
</span>
<% } %>
</h3>
<div class="item-actions">
<ul class="actions-list">
<li class="action-item action-delete">
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
<i class="icon-remove"></i>
<span class="sr"><%= gettext('Delete') %></span>
</a>
</li>
</ul>
</div>
<div class="<%= xblockType %>-header-actions">
<ul class="actions-list">
<li class="action-item action-delete">
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
<i class="icon icon-trash"></i>
<span class="sr action-button-text"><%= gettext('Delete') %></span>
</a>
</li>
</ul>
</div>
<div class="wrapper-xblock-header-secondary">
<% if (xblockInfo.get('edited_on')) { %>
<div class="meta-info">
<% if (xblockInfo.get('published')) { %>
<i class="icon-check"></i>
<%= gettext('Released:') %> Dec 31, 2015 at 21:00 UTC
<% } else { %>
<i class="icon-time"></i>
<%= gettext('Scheduled:') %> Dec 31, 2015 at 21:00 UTC
<% } %>
</div>
<% if (statusMessage) { %>
<div class="<%= xblockType %>-status">
<% if (category !== 'vertical') { %>
<div class="status-release">
<p>
<span class="sr status-release-label">Release Status:</span>
<span class="status-release-value">
<% 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 class="item-actions">
<ul class="actions-list">
</ul>
<div class="status-message">
<i class="icon <%= statusIconClass %>"></i>
<p class="status-message-copy"><%- statusMessage %></p>
</div>
</div>
</div>
<% } %>
<% } %>
<% 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.") %>
<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 %>">
<i class="icon-plus"></i><%= addChildLabel %>
<i class="icon icon-plus"></i><%= addChildLabel %>
</a>
</p>
</div>
<% } else { %>
<ol class="sortable-list sortable-<%= xblockType %>-list">
</ol>
<% if (childType) { %>
<div class="add-xblock-component">
<a href="#" class="add-button" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %>
</a>
</div>
<% } %>
<div class="outline-content <%= xblockType %>-content">
<ol class="<%= listType %> is-sortable">
</ol>
<% if (childType) { %>
<div class="add-<%= childType %> add-item">
<a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon icon-plus"></i><%= addChildLabel %>
</a>
</div>
<% } %>
</div>
<% } %>
<% if (parentInfo) { %>
......
......@@ -11,13 +11,13 @@
<h3 class="sr">Page Actions</h3>
<ul>
<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="expand-all"><i class="icon-arrow-down"></i> <span class="label">Expand All Sections</span></span>
</a>
</li>
<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
</a>
</li>
......@@ -33,10 +33,10 @@
<section class="content">
<article class="content-primary" role="main">
<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">
<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
</a>
</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">
<p class="copy">
<% if (published) {
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
<p class="copy"><%= copy %></p>
</div>
<%
var publishClasses = "";
var title = gettext("Draft (Never published)");
if (published) {
if (published && hasChanges) {
publishClasses = publishClasses + " is-draft";
title = gettext("Draft (Unpublished changes)");
} else {
publishClasses = publishClasses + " is-published";
title = gettext("Published");
}
}
if (releaseDate) {
publishClasses = publishClasses + " is-scheduled";
var publishClass = '';
if (publishState === 'staff_only') {
publishClass = 'is-staff-only';
} else if (publishState === 'live') {
publishClass = 'is-live is-published is-released';
} else if (publishState === 'ready') {
publishClass = 'is-ready is-published';
} else if (publishState === 'has_unpublished_content') {
publishClass = 'has-warnings is-draft';
}
if (visibleToStaffOnly) {
publishClasses = publishClasses + " is-staff-only";
var title = gettext("Draft (Never published)");
if (publishState === '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>
<%= title %>
</h3>
<div class="wrapper-last-draft bar-mod-content">
<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") %>
<%= interpolate(message, {
last_saved_date: '<span class="date">' + editedOn + '</span>',
......@@ -42,17 +56,7 @@ if (visibleToStaffOnly) {
</div>
<div class="wrapper-release bar-mod-content">
<h5 class="title">
<% if (published && releaseDate) {
if (releasedToStudents) { %>
<%= gettext("Released:") %>
<% } else { %>
<%= gettext("Scheduled:") %>
<% }
} else { %>
<%= gettext("Release:") %>
<% } %>
</h5>
<h5 class="title"><%= releaseLabel %></h5>
<p class="copy">
<% if (releaseDate) { %>
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
......@@ -87,12 +91,12 @@ if (visibleToStaffOnly) {
<div class="wrapper-pub-actions bar-mod-actions">
<ul class="action-list">
<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") %>
</a>
</li>
<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") %>
</a>
</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) { %>
<li class="outline-item outline-item-<%= xblockType %>"
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
<div class="wrapper-xblock-header">
<div class="wrapper-xblock-header-primary">
<h3 class="xblock-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
<li class="outline-item outline-<%= xblockType %> <%= publishClass %>"
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
<div class="<%= xblockType %>-header">
<h3 class="<%= xblockType %>-header-details">
<span class="unit-title item-title">
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
</span>
</h3>
</div>
</div>
<% } %>
<ol class="sortable-list sortable-<%= xblockType %>-list">
</ol>
<% if (childType) { %>
<div class="add-xblock-component">
<a href="#" class="add-button" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon-plus"></i><%= addChildLabel %>
</a>
</div>
<% } %>
<div class="<%= xblockType %>-content outline-content">
<ol class="<%= listType %>">
</ol>
<% if (childType) { %>
<div class="add-<%= childType %> add-item">
<a href="#" class="button button-new" data-category="<%= childCategory %>"
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>">
<i class="icon icon-plus"></i><%= addChildLabel %>
</a>
</div>
<% } %>
</div>
<% if (parentInfo) { %>
</li>
</li>
<% } %>
<% 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') %>">
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon-caret-right"></i></span>
......@@ -56,7 +56,7 @@
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
<div class="no-content add-xblock-component">
<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 %>">
<i class="icon-plus"></i><%= addChildLabel %>
</a>
......@@ -68,7 +68,7 @@
<% if (childType) { %>
<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 %>">
<i class="icon-plus"></i><%= addChildLabel %>
</a>
......
......@@ -89,11 +89,11 @@ class ModuleStoreEnum(object):
# user ID to use for tests that do not have a django user available
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'
private = 'private'
......@@ -301,10 +301,10 @@ class ModuleStoreRead(object):
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
LegacyPublishState.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
LegacyPublishState.public - content is locked and deployed to LMS
LegacyPublishState.private - content is editable and not deployed to LMS
"""
pass
......@@ -522,7 +522,7 @@ class ModuleStoreReadBase(ModuleStoreRead):
"""
Returns PublishState.public since this is a read-only store.
"""
return PublishState.public
return LegacyPublishState.public
def heartbeat(self):
"""
......
......@@ -439,10 +439,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
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
LegacyPublishState.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
LegacyPublishState.public - content is locked and deployed to LMS
LegacyPublishState.private - content is editable and not deployed to LMS
"""
course_id = xblock.scope_ids.usage_id.course_key
store = self._get_modulestore_for_courseid(course_id)
......
......@@ -11,7 +11,7 @@ import logging
from opaque_keys.edx.locations import Location
from xmodule.exceptions import InvalidVersionError
from xmodule.modulestore import PublishState, ModuleStoreEnum
from xmodule.modulestore import LegacyPublishState, ModuleStoreEnum
from xmodule.modulestore.exceptions import (
ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError
)
......@@ -613,7 +613,7 @@ class DraftModuleStore(MongoModuleStore):
return False
# 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
# if this block doesn't have changes, then check its children
elif item.has_children:
......@@ -792,10 +792,10 @@ class DraftModuleStore(MongoModuleStore):
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
LegacyPublishState.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
LegacyPublishState.public - content is locked and deployed to LMS
LegacyPublishState.private - content is editable and not deployed to LMS
"""
if getattr(xblock, 'is_draft', False):
published_xblock_location = as_published(xblock.location)
......@@ -803,11 +803,11 @@ class DraftModuleStore(MongoModuleStore):
{'_id': published_xblock_location.to_deprecated_son()}
)
if published_item is None:
return PublishState.private
return LegacyPublishState.private
else:
return PublishState.draft
return LegacyPublishState.draft
else:
return PublishState.public
return LegacyPublishState.public
def _verify_branch_setting(self, expected_branch_setting):
"""
......
......@@ -4,7 +4,7 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore
from ..exceptions import ItemNotFoundError
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.draft_and_published import ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
......@@ -251,10 +251,11 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Returns whether this xblock is draft, public, or private.
Returns:
PublishState.draft - published exists and is different from draft
PublishState.public - published exists and is the same as draft
PublishState.private - no published version exists
LegacyPublishState.draft - published exists and is different from draft
LegacyPublishState.public - published exists and is the same as draft
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):
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)
......@@ -271,13 +272,13 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
if not published_head:
# published version does not exist
return PublishState.private
return LegacyPublishState.private
elif get_version(draft_head) == get_version(published_head):
# published and draft versions are equal
return PublishState.public
return LegacyPublishState.public
else:
# published and draft versions differ
return PublishState.draft
return LegacyPublishState.draft
def convert_to_draft(self, location, user_id):
"""
......
......@@ -9,7 +9,7 @@ from pytz import UTC
from xmodule.tests import DATA_DIR
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.exceptions import InvalidVersionError
......@@ -991,22 +991,22 @@ class TestMixedModuleStore(unittest.TestCase):
item_location = item.location.version_agnostic()
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):
self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
self.assertEquals(self.store.compute_publish_state(item), LegacyPublishState.private)
# Private -> Public
self.store.publish(item_location, self.user_id)
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
self.store.unpublish(item_location, self.user_id)
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
self.store.publish(item_location, self.user_id)
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
# Note: This is where Split and Mongo differ
......@@ -1014,14 +1014,14 @@ class TestMixedModuleStore(unittest.TestCase):
item = self.store.get_item(item_location)
self.assertEquals(
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
item.display_name = 'new name'
item = self.store.update_item(item, self.user_id)
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')
def test_auto_publish(self, default_ms):
......
......@@ -113,7 +113,7 @@ class CourseOutlineContainer(CourseOutlineItem):
"""
click_css(
self,
self._bounded_selector(".add-xblock-component a.add-button"),
self._bounded_selector(".add-item a.button-new"),
require_notification=require_notification,
)
......@@ -125,7 +125,7 @@ class CourseOutlineContainer(CourseOutlineItem):
self.browser.execute_script("jQuery.fx.off = true;")
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()
currently_expanded = subsection_expanded()
......@@ -171,8 +171,8 @@ class CourseOutlineUnit(CourseOutlineChild):
PageObject that wraps a unit link on the Studio Course Outline page.
"""
url = None
BODY_SELECTOR = '.outline-item-unit'
NAME_SELECTOR = '.xblock-title a'
BODY_SELECTOR = '.outline-unit'
NAME_SELECTOR = '.unit-title a'
def go_to(self):
"""
......@@ -191,7 +191,9 @@ class CourseOutlineSubsection(CourseOutlineChild, CourseOutlineContainer):
"""
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
def unit(self, title):
......@@ -224,7 +226,9 @@ class CourseOutlineSection(CourseOutlineChild, CourseOutlineContainer):
:class`.PageObject` that wraps a section block on the Studio Course Outline page.
"""
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
def subsection(self, title):
......@@ -268,7 +272,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
url_path = "course"
CHILD_CLASS = CourseOutlineSection
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):
return self.q(css='body.view-outline').present
......@@ -337,7 +341,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
"""
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):
"""
......
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