Commit cbbf38b4 by Christina Roberts

Merge pull request #2838 from edx/andya/container-publishing

Support publishing of nested xblocks
parents e457135e 5166461c
...@@ -194,30 +194,35 @@ def course_image_url(course): ...@@ -194,30 +194,35 @@ def course_image_url(course):
return path return path
class UnitState(object): 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.
"""
draft = 'draft' draft = 'draft'
private = 'private' private = 'private'
public = 'public' public = 'public'
def compute_unit_state(unit): def compute_publish_state(xblock):
""" """
Returns whether this unit is 'draft', 'public', or 'private'. Returns whether this xblock is 'draft', 'public', or 'private'.
'draft' content is in the process of being edited, but still has a previous 'draft' content is in the process of being edited, but still has a previous
version visible in the LMS version visible in the LMS
'public' content is locked and visible in the LMS 'public' content is locked and visible in the LMS
'private' content is editabled and not visible in the LMS 'private' content is editable and not visible in the LMS
""" """
if getattr(unit, 'is_draft', False): if getattr(xblock, 'is_draft', False):
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(xblock.location)
return UnitState.draft return PublishState.draft
except ItemNotFoundError: except ItemNotFoundError:
return UnitState.private return PublishState.private
else: else:
return UnitState.public return PublishState.public
def add_extra_panel_tab(tab_type, course): def add_extra_panel_tab(tab_type, course):
......
...@@ -26,7 +26,7 @@ from xblock.runtime import Mixologist ...@@ -26,7 +26,7 @@ from xblock.runtime import Mixologist
from lms.lib.xblock.runtime import unquote_slashes from lms.lib.xblock.runtime import unquote_slashes
from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState, get_modulestore from contentstore.utils import get_lms_link_for_item, compute_publish_state, PublishState, get_modulestore
from contentstore.views.helpers import get_parent_xblock from contentstore.views.helpers import get_parent_xblock
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
...@@ -107,8 +107,8 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_ ...@@ -107,8 +107,8 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
can_view_live = False can_view_live = False
subsection_units = item.get_children() subsection_units = item.get_children()
for unit in subsection_units: for unit in subsection_units:
state = compute_unit_state(unit) state = compute_publish_state(unit)
if state == UnitState.public or state == UnitState.draft: if state in (PublishState.public, PublishState.draft):
can_view_live = True can_view_live = True
break break
...@@ -282,7 +282,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N ...@@ -282,7 +282,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
), ),
'section': containing_section, 'section': containing_section,
'new_unit_category': 'vertical', 'new_unit_category': 'vertical',
'unit_state': compute_unit_state(item), 'unit_state': compute_publish_state(item),
'published_date': ( 'published_date': (
get_default_time_display(item.published_date) get_default_time_display(item.published_date)
if item.published_date is not None else None if item.published_date is not None else None
...@@ -322,6 +322,7 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g ...@@ -322,6 +322,7 @@ def container_handler(request, tag=None, package_id=None, branch=None, version_g
'context_course': course, 'context_course': course,
'xblock': xblock, 'xblock': xblock,
'xblock_locator': locator, 'xblock_locator': locator,
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
'ancestor_xblocks': ancestor_xblocks, 'ancestor_xblocks': ancestor_xblocks,
}) })
else: else:
......
...@@ -97,5 +97,5 @@ def xblock_studio_url(xblock, course=None): ...@@ -97,5 +97,5 @@ def xblock_studio_url(xblock, course=None):
course_id = None course_id = None
if course: if course:
course_id = course.location.course_id course_id = course.location.course_id
locator = loc_mapper().translate_location(course_id, xblock.location) locator = loc_mapper().translate_location(course_id, xblock.location, published=False)
return locator.url_reverse(prefix) return locator.url_reverse(prefix)
...@@ -297,9 +297,9 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta ...@@ -297,9 +297,9 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
if publish == 'make_private': if publish == 'make_private':
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location)) _xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
elif publish == 'create_draft': elif publish == 'create_draft':
# This clones the existing item location to a draft location (the draft is # This recursively clones the existing item location to a draft location (the draft is
# implicit, because modulestore is a Draft modulestore) # implicit, because modulestore is a Draft modulestore)
modulestore().convert_to_draft(item_location) _xmodule_recurse(existing_item, lambda i: modulestore().convert_to_draft(i.location))
if data: if data:
# TODO Allow any scope.content fields not just "data" (exactly like the get below this) # TODO Allow any scope.content fields not just "data" (exactly like the get below this)
......
...@@ -28,9 +28,9 @@ class ContainerViewTestCase(CourseTestCase): ...@@ -28,9 +28,9 @@ class ContainerViewTestCase(CourseTestCase):
def test_container_html(self): def test_container_html(self):
self._test_html_content( self._test_html_content(
self.child_vertical, self.child_vertical,
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical"/>', expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical"/>',
expected_breadcrumbs=( expected_breadcrumbs=(
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit"\s*' r'<a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*' r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'), r'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'),
) )
...@@ -46,11 +46,11 @@ class ContainerViewTestCase(CourseTestCase): ...@@ -46,11 +46,11 @@ class ContainerViewTestCase(CourseTestCase):
category="html", display_name="Child HTML") category="html", display_name="Child HTML")
self._test_html_content( self._test_html_content(
xblock_with_child, xblock_with_child,
expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/published/block/Wrapper"/>', expected_section_tag='<section class="wrapper-xblock level-page" data-locator="MITx.999.Robot_Super_Course/branch/draft/block/Wrapper"/>',
expected_breadcrumbs=( expected_breadcrumbs=(
r'<a href="/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit"\s*' r'<a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit"\s*'
r'class="navigation-link navigation-parent">Unit</a>\s*' r'class="navigation-link navigation-parent">Unit</a>\s*'
r'<a href="/container/MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical"\s*' r'<a href="/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical"\s*'
r'class="navigation-link navigation-parent">Child Vertical</a>\s*' r'class="navigation-link navigation-parent">Child Vertical</a>\s*'
r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'), r'<a href="#" class="navigation-link navigation-current">Wrapper</a>'),
) )
...@@ -67,3 +67,6 @@ class ContainerViewTestCase(CourseTestCase): ...@@ -67,3 +67,6 @@ class ContainerViewTestCase(CourseTestCase):
self.assertIn(expected_section_tag, html) self.assertIn(expected_section_tag, html)
# Verify the navigation link at the top of the page is correct. # Verify the navigation link at the top of the page is correct.
self.assertRegexpMatches(html, expected_breadcrumbs) self.assertRegexpMatches(html, expected_breadcrumbs)
# Verify the link that allows users to change publish status.
expected_unit_link = 'This content is published with unit <a href="/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit">Unit</a>.'
self.assertIn(expected_unit_link, html)
...@@ -16,7 +16,7 @@ class HelpersTestCase(CourseTestCase): ...@@ -16,7 +16,7 @@ class HelpersTestCase(CourseTestCase):
# Verify course URL # Verify course URL
self.assertEqual(xblock_studio_url(course), self.assertEqual(xblock_studio_url(course),
u'/course/MITx.999.Robot_Super_Course/branch/published/block/Robot_Super_Course') u'/course/MITx.999.Robot_Super_Course/branch/draft/block/Robot_Super_Course')
# Verify chapter URL # Verify chapter URL
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter', chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
...@@ -34,17 +34,17 @@ class HelpersTestCase(CourseTestCase): ...@@ -34,17 +34,17 @@ class HelpersTestCase(CourseTestCase):
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical', vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
display_name='Unit') display_name='Unit')
self.assertEqual(xblock_studio_url(vertical), self.assertEqual(xblock_studio_url(vertical),
u'/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit') u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit')
self.assertEqual(xblock_studio_url(vertical, course), self.assertEqual(xblock_studio_url(vertical, course),
u'/unit/MITx.999.Robot_Super_Course/branch/published/block/Unit') u'/unit/MITx.999.Robot_Super_Course/branch/draft/block/Unit')
# Verify child vertical URL # Verify child vertical URL
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical', child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
display_name='Child Vertical') display_name='Child Vertical')
self.assertEqual(xblock_studio_url(child_vertical), self.assertEqual(xblock_studio_url(child_vertical),
u'/container/MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical') u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical')
self.assertEqual(xblock_studio_url(child_vertical, course), self.assertEqual(xblock_studio_url(child_vertical, course),
u'/container/MITx.999.Robot_Super_Course/branch/published/block/Child_Vertical') u'/container/MITx.999.Robot_Super_Course/branch/draft/block/Child_Vertical')
# Verify video URL # Verify video URL
video = ItemFactory.create(parent_location=child_vertical.location, category="video", video = ItemFactory.create(parent_location=child_vertical.location, category="video",
......
...@@ -15,6 +15,7 @@ from django.test.client import RequestFactory ...@@ -15,6 +15,7 @@ from django.test.client import RequestFactory
from contentstore.views.component import component_handler from contentstore.views.component import component_handler
from contentstore.tests.utils import CourseTestCase from contentstore.tests.utils import CourseTestCase
from contentstore.utils import compute_publish_state, PublishState
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.capa_module import CapaDescriptor from xmodule.capa_module import CapaDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -153,7 +154,7 @@ class GetItem(ItemTest): ...@@ -153,7 +154,7 @@ class GetItem(ItemTest):
html, html,
# The instance of the wrapper class will have an auto-generated ID (wrapperxxx). Allow anything # The instance of the wrapper class will have an auto-generated ID (wrapperxxx). Allow anything
# for the 3 characters after wrapper. # for the 3 characters after wrapper.
(r'"/container/MITx.999.Robot_Super_Course/branch/published/block/wrapper.{3}" class="action-button">\s*' (r'"/container/MITx.999.Robot_Super_Course/branch/draft/block/wrapper.{3}" class="action-button">\s*'
'<span class="action-button-text">View</span>') '<span class="action-button-text">View</span>')
) )
...@@ -663,6 +664,7 @@ class TestEditItem(ItemTest): ...@@ -663,6 +664,7 @@ class TestEditItem(ItemTest):
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
# Activate the editing view # Activate the editing view
view_url = '/xblock/{locator}/studio_view'.format(locator=self.problem_locator)
resp = self.client.get(view_url, HTTP_ACCEPT='application/json') resp = self.client.get(view_url, HTTP_ACCEPT='application/json')
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -671,6 +673,49 @@ class TestEditItem(ItemTest): ...@@ -671,6 +673,49 @@ class TestEditItem(ItemTest):
draft = self.get_item_from_modulestore(self.problem_locator, True) draft = self.get_item_from_modulestore(self.problem_locator, True)
self.assertNotEqual(draft.data, published.data) self.assertNotEqual(draft.data, published.data)
def test_publish_states_of_nested_xblocks(self):
""" Test publishing of a unit page containing a nested xblock """
resp = self.create_xblock(parent_locator=self.seq_locator, display_name='Test Unit', category='vertical')
unit_locator = self.response_locator(resp)
resp = self.create_xblock(parent_locator=unit_locator, category='wrapper')
wrapper_locator = self.response_locator(resp)
resp = self.create_xblock(parent_locator=wrapper_locator, category='html')
html_locator = self.response_locator(resp)
# The unit and its children should be private initially
unit_update_url = '/xblock/' + unit_locator
unit = self.get_item_from_modulestore(unit_locator, True)
html = self.get_item_from_modulestore(html_locator, True)
self.assertEqual(compute_publish_state(unit), PublishState.private)
self.assertEqual(compute_publish_state(html), PublishState.private)
# Make the unit public and verify that the problem is also made public
resp = self.client.ajax_post(
unit_update_url,
data={'publish': 'make_public'}
)
self.assertEqual(resp.status_code, 200)
unit = self.get_item_from_modulestore(unit_locator, True)
html = self.get_item_from_modulestore(html_locator, True)
self.assertEqual(compute_publish_state(unit), PublishState.public)
self.assertEqual(compute_publish_state(html), PublishState.public)
# Make a draft for the unit and verify that the problem also has a draft
resp = self.client.ajax_post(
unit_update_url,
data={
'id': unit_locator,
'metadata': {},
'publish': 'create_draft'
}
)
self.assertEqual(resp.status_code, 200)
unit = self.get_item_from_modulestore(unit_locator, True)
html = self.get_item_from_modulestore(html_locator, True)
self.assertEqual(compute_publish_state(unit), PublishState.draft)
self.assertEqual(compute_publish_state(html), PublishState.draft)
@ddt.ddt @ddt.ddt
class TestComponentHandler(TestCase): class TestComponentHandler(TestCase):
......
...@@ -30,11 +30,22 @@ body.view-container { ...@@ -30,11 +30,22 @@ body.view-container {
label { label {
@extend %t-title8; @extend %t-title8;
} }
.bit-publishing {
margin-bottom: $baseline;
border-top: 5px solid $blue;
background-color: $white;
padding: ($baseline*.75) ($baseline*.75) ($baseline) ($baseline*.75);
.copy {
@extend %t-copy-sub1;
}
}
} }
} }
// UI: xblock rendering // UI: xblock rendering
body.view-container .content-primary{ body.view-container .content-primary {
.wrapper-xblock { .wrapper-xblock {
@extend %wrap-xblock; @extend %wrap-xblock;
......
...@@ -79,6 +79,15 @@ xblock_info = { ...@@ -79,6 +79,15 @@ xblock_info = {
<section class="wrapper-xblock level-page" data-locator="${xblock_locator}"/> <section class="wrapper-xblock level-page" data-locator="${xblock_locator}"/>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit-publishing">
<h3 class="title-3">${_("Publishing Status")}</h3>
<p class="copy">${_('This content is published with unit {unit_name}.').format(
unit_name=u'<a href="{unit_address}">{unit_display_name}</a>'.format(
unit_address=xblock_studio_url(unit),
unit_display_name=unit.display_name_with_default,
)
)}</p>
</div>
<div class="bit"> <div class="bit">
<h3 class="title-3">${_("What can I do on this page?")}</h3> <h3 class="title-3">${_("What can I do on this page?")}</h3>
<ul class="list-details"> <ul class="list-details">
......
<%inherit file="../../base.html" /> <%inherit file="../../base.html" />
<%! <%!
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
%> %>
<%block name="title">${_("Container")}</%block> <%block name="title">Container</%block>
<%block name="bodyclass">is-signedin course uploads view-container</%block> <%block name="bodyclass">is-signedin course uploads view-container</%block>
<%namespace name='static' file='../../static_content.html'/> <%namespace name='static' file='../../static_content.html'/>
...@@ -426,6 +425,10 @@ from django.utils.translation import ugettext as _ ...@@ -426,6 +425,10 @@ from django.utils.translation import ugettext as _
</section> </section>
</article> </article>
<aside class="content-supplementary" role="complimentary"> <aside class="content-supplementary" role="complimentary">
<div class="bit-publishing">
<h3 class="title-3">Publishing Status</h3>
<p class="copy">This content is published with unit <a href="">Unit 1</a>. To make changes to the content of this container, place <a href="">Unit 1</a> in draft mode.</p>
</div>
<div class="bit"> <div class="bit">
<h3 class="title-3">Container Reference Page</h3> <h3 class="title-3">Container Reference Page</h3>
<ul class="list-details"> <ul class="list-details">
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %> <%! from django.core.urlresolvers import reverse %>
<%! from contentstore.utils import compute_unit_state %> <%! from contentstore.utils import compute_publish_state %>
<%! from xmodule.modulestore.django import loc_mapper %> <%! from xmodule.modulestore.django import loc_mapper %>
<!-- <!--
...@@ -25,7 +25,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -25,7 +25,7 @@ This def will enumerate through a passed in subsection and list all of the units
<%include file="_ui-dnd-indicator-before.html" /> <%include file="_ui-dnd-indicator-before.html" />
<% <%
unit_state = compute_unit_state(unit) unit_state = compute_publish_state(unit)
if unit.location == selected: if unit.location == selected:
selected_class = 'editing' selected_class = 'editing'
else: else:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment