Merge pull request #2838 from edx/andya/container-publishing
Support publishing of nested xblocks
This commit is contained in:
@@ -194,30 +194,35 @@ def course_image_url(course):
|
||||
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'
|
||||
private = 'private'
|
||||
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
|
||||
version 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:
|
||||
modulestore('direct').get_item(unit.location)
|
||||
return UnitState.draft
|
||||
modulestore('direct').get_item(xblock.location)
|
||||
return PublishState.draft
|
||||
except ItemNotFoundError:
|
||||
return UnitState.private
|
||||
return PublishState.private
|
||||
else:
|
||||
return UnitState.public
|
||||
return PublishState.public
|
||||
|
||||
|
||||
def add_extra_panel_tab(tab_type, course):
|
||||
|
||||
@@ -26,7 +26,7 @@ from xblock.runtime import Mixologist
|
||||
|
||||
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 models.settings.course_grading import CourseGradingModel
|
||||
@@ -107,8 +107,8 @@ def subsection_handler(request, tag=None, package_id=None, branch=None, version_
|
||||
can_view_live = False
|
||||
subsection_units = item.get_children()
|
||||
for unit in subsection_units:
|
||||
state = compute_unit_state(unit)
|
||||
if state == UnitState.public or state == UnitState.draft:
|
||||
state = compute_publish_state(unit)
|
||||
if state in (PublishState.public, PublishState.draft):
|
||||
can_view_live = True
|
||||
break
|
||||
|
||||
@@ -282,7 +282,7 @@ def unit_handler(request, tag=None, package_id=None, branch=None, version_guid=N
|
||||
),
|
||||
'section': containing_section,
|
||||
'new_unit_category': 'vertical',
|
||||
'unit_state': compute_unit_state(item),
|
||||
'unit_state': compute_publish_state(item),
|
||||
'published_date': (
|
||||
get_default_time_display(item.published_date)
|
||||
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
|
||||
'context_course': course,
|
||||
'xblock': xblock,
|
||||
'xblock_locator': locator,
|
||||
'unit': None if not ancestor_xblocks else ancestor_xblocks[0],
|
||||
'ancestor_xblocks': ancestor_xblocks,
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -97,5 +97,5 @@ def xblock_studio_url(xblock, course=None):
|
||||
course_id = None
|
||||
if course:
|
||||
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)
|
||||
|
||||
@@ -297,9 +297,9 @@ def _save_item(request, usage_loc, item_location, data=None, children=None, meta
|
||||
if publish == 'make_private':
|
||||
_xmodule_recurse(existing_item, lambda i: modulestore().unpublish(i.location))
|
||||
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)
|
||||
modulestore().convert_to_draft(item_location)
|
||||
_xmodule_recurse(existing_item, lambda i: modulestore().convert_to_draft(i.location))
|
||||
|
||||
if data:
|
||||
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
|
||||
|
||||
@@ -28,9 +28,9 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
def test_container_html(self):
|
||||
self._test_html_content(
|
||||
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=(
|
||||
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'<a href="#" class="navigation-link navigation-current">Child Vertical</a>'),
|
||||
)
|
||||
@@ -46,11 +46,11 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
category="html", display_name="Child HTML")
|
||||
self._test_html_content(
|
||||
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=(
|
||||
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'<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'<a href="#" class="navigation-link navigation-current">Wrapper</a>'),
|
||||
)
|
||||
@@ -67,3 +67,6 @@ class ContainerViewTestCase(CourseTestCase):
|
||||
self.assertIn(expected_section_tag, html)
|
||||
# Verify the navigation link at the top of the page is correct.
|
||||
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):
|
||||
|
||||
# Verify course URL
|
||||
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
|
||||
chapter = ItemFactory.create(parent_location=self.course.location, category='chapter',
|
||||
@@ -34,17 +34,17 @@ class HelpersTestCase(CourseTestCase):
|
||||
vertical = ItemFactory.create(parent_location=sequential.location, category='vertical',
|
||||
display_name='Unit')
|
||||
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),
|
||||
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
|
||||
child_vertical = ItemFactory.create(parent_location=vertical.location, category='vertical',
|
||||
display_name='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),
|
||||
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
|
||||
video = ItemFactory.create(parent_location=child_vertical.location, category="video",
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.test.client import RequestFactory
|
||||
from contentstore.views.component import component_handler
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import compute_publish_state, PublishState
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -153,7 +154,7 @@ class GetItem(ItemTest):
|
||||
html,
|
||||
# The instance of the wrapper class will have an auto-generated ID (wrapperxxx). Allow anything
|
||||
# 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>')
|
||||
)
|
||||
|
||||
@@ -663,6 +664,7 @@ class TestEditItem(ItemTest):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# 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')
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
@@ -671,6 +673,49 @@ class TestEditItem(ItemTest):
|
||||
draft = self.get_item_from_modulestore(self.problem_locator, True)
|
||||
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
|
||||
class TestComponentHandler(TestCase):
|
||||
|
||||
@@ -30,11 +30,22 @@ body.view-container {
|
||||
label {
|
||||
@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
|
||||
body.view-container .content-primary{
|
||||
body.view-container .content-primary {
|
||||
|
||||
.wrapper-xblock {
|
||||
@extend %wrap-xblock;
|
||||
|
||||
@@ -79,6 +79,15 @@ xblock_info = {
|
||||
<section class="wrapper-xblock level-page" data-locator="${xblock_locator}"/>
|
||||
</article>
|
||||
<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">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<ul class="list-details">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<%inherit file="../../base.html" />
|
||||
<%!
|
||||
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>
|
||||
|
||||
<%namespace name='static' file='../../static_content.html'/>
|
||||
@@ -426,6 +425,10 @@ from django.utils.translation import ugettext as _
|
||||
</section>
|
||||
</article>
|
||||
<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">
|
||||
<h3 class="title-3">Container Reference Page</h3>
|
||||
<ul class="list-details">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%! 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 %>
|
||||
|
||||
<!--
|
||||
@@ -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" />
|
||||
|
||||
<%
|
||||
unit_state = compute_unit_state(unit)
|
||||
unit_state = compute_publish_state(unit)
|
||||
if unit.location == selected:
|
||||
selected_class = 'editing'
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user