Integrate visual styling into the course outline
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for items views."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import ddt
|
||||
|
||||
from mock import patch
|
||||
@@ -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)
|
||||
|
||||
@@ -24,21 +24,6 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
* An optional object with information about each of the ancestors.
|
||||
*/
|
||||
"ancestor_info": null,
|
||||
/**
|
||||
* True iff:
|
||||
* 1) Edits have been made to the xblock and no published version exists.
|
||||
* 2) Edits have been made to the xblock since the last published version.
|
||||
*/
|
||||
"has_changes": null,
|
||||
/**
|
||||
* True iff a published version of the xblock exists.
|
||||
*/
|
||||
"published": null,
|
||||
/**
|
||||
* If true, only course staff can see the xblock regardless of publish status or
|
||||
* release date status.
|
||||
*/
|
||||
"visible_to_staff_only": null,
|
||||
/**
|
||||
* Date of the last edit to this xblock or any of its descendants.
|
||||
*/
|
||||
@@ -47,6 +32,10 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
* User who last edited the xblock or any of its descendants.
|
||||
*/
|
||||
"edited_by":null,
|
||||
/**
|
||||
* True iff a published version of the xblock exists.
|
||||
*/
|
||||
"published": null,
|
||||
/**
|
||||
* Date of the last publish of this xblock, or null if never published.
|
||||
*/
|
||||
@@ -55,6 +44,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
|
||||
* User who last published the xblock, or null if never published.
|
||||
*/
|
||||
"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.
|
||||
*/
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,79 +1,141 @@
|
||||
<%
|
||||
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>
|
||||
<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>
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="item-actions">
|
||||
<ul class="actions-list">
|
||||
</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>
|
||||
<% 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="status-message">
|
||||
<i class="icon <%= statusIconClass %>"></i>
|
||||
<p class="status-message-copy"><%- statusMessage %></p>
|
||||
</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>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<%
|
||||
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>
|
||||
<p class="copy"><%= copy %></p>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
<%
|
||||
var publishClasses = "";
|
||||
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';
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
if (visibleToStaffOnly) {
|
||||
publishClasses = publishClasses + " is-staff-only";
|
||||
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>
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
<%
|
||||
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>
|
||||
<% } %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% 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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user