New Publishing controls on Unit page.

STUD-1707
This commit is contained in:
Andy Armstrong
2014-07-03 10:44:35 -04:00
committed by cahrens
parent b1eccdf2d4
commit d65e887d1a
19 changed files with 773 additions and 217 deletions

View File

@@ -152,6 +152,7 @@ def xml_only_video(step):
category='video',
data='<video youtube="1.00:%s"></video>' % youtube_id,
modulestore=store,
user_id=world.scenario_dict["USER"].id
)

View File

@@ -23,6 +23,7 @@ from xblock.runtime import Mixologist
from contentstore.utils import get_lms_link_for_item, compute_publish_state
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
from contentstore.views.item import create_xblock_info
from models.settings.course_grading import CourseGradingModel
from opaque_keys.edx.keys import UsageKey
@@ -148,7 +149,7 @@ def container_handler(request, usage_key_string):
usage_key = UsageKey.from_string(usage_key_string)
try:
course, xblock, __ = _get_item_in_course(request, usage_key)
course, xblock, lms_link = _get_item_in_course(request, usage_key)
except ItemNotFoundError:
return HttpResponseBadRequest()
@@ -166,15 +167,38 @@ def container_handler(request, usage_key_string):
parent = get_parent_xblock(parent)
ancestor_xblocks.reverse()
subsection = get_parent_xblock(unit) if unit else None
section = get_parent_xblock(subsection) if subsection else None
# TODO: correct with publishing story.
unit_publish_state = 'draft'
assert unit is not None, "Could not determine unit page"
subsection = get_parent_xblock(unit)
assert subsection is not None, "Could not determine parent subsection from unit " + unicode(unit.location)
section = get_parent_xblock(subsection)
assert section is not None, "Could not determine ancestor section from unit " + unicode(unit.location)
xblock_info = create_xblock_info(usage_key, xblock)
# Create the link for preview.
preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE')
# need to figure out where this item is in the list of children as the
# preview will need this
index = 1
for child in subsection.get_children():
if child.location == unit.location:
break
index += 1
preview_lms_link = (
u'//{preview_lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'
).format(
preview_lms_base=preview_lms_base,
lms_base=settings.LMS_BASE,
org=course.location.org,
course=course.location.course,
course_name=course.location.name,
section=section.location.name,
subsection=subsection.location.name,
index=index
)
return render_to_response('container.html', {
'context_course': course, # Needed only for display of menus at top of page.
'xblock': xblock,
'unit_publish_state': unit_publish_state,
'xblock_locator': xblock.location,
'unit': unit,
'is_unit_page': is_unit_page,
@@ -183,6 +207,9 @@ def container_handler(request, usage_key_string):
'new_unit_category': 'vertical',
'ancestor_xblocks': ancestor_xblocks,
'component_templates': json.dumps(component_templates),
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
})
else:
return HttpResponseBadRequest("Only supports HTML requests")

View File

@@ -23,9 +23,13 @@ import xmodule
from xmodule.tabs import StaticTab, CourseTabList
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, DuplicateItemError
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW
from contentstore.utils import compute_publish_state
from xmodule.modulestore import PublishState
from django.contrib.auth.models import User
from util.date_utils import get_default_time_display
from util.json_request import expect_json, JsonResponse
@@ -92,7 +96,7 @@ def xblock_handler(request, usage_key_string):
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
:graderType: change how this unit is graded
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
:publish: can be only one value-- 'make_public'
The JSON representation on the updated xblock (minus children) is returned.
if usage_key_string is not specified, create a new xblock instance, either by duplicating
@@ -183,7 +187,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
if 'application/json' in accept_header:
store = modulestore()
xblock = store.get_item(usage_key)
is_read_only = _is_xblock_read_only(xblock)
container_views = ['container_preview', 'reorderable_container_child_preview']
# wrap the generated fragment in the xmodule_editor div so that the javascript
@@ -216,7 +219,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
context = {
'is_pages_view': is_pages_view, # This setting disables the recursive wrapping of xblocks
'is_unit_page': is_unit(xblock),
'read_only': is_read_only,
'root_xblock': xblock if (view_name == 'container_preview') else None,
'reorderable_items': reorderable_items
}
@@ -249,19 +251,6 @@ def xblock_view_handler(request, usage_key_string, view_name):
return HttpResponse(status=406)
def _is_xblock_read_only(xblock):
"""
Returns true if the specified xblock is read-only, meaning that it cannot be edited.
"""
# We allow direct editing of xblocks in DIRECT_ONLY_CATEGORIES (for example, static pages).
# if xblock.category in DIRECT_ONLY_CATEGORIES:
# return False
# component_publish_state = compute_publish_state(xblock)
# return component_publish_state == PublishState.public
# TODO: correct with publishing story.
return False
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None,
grader_type=None, publish=None):
"""
@@ -287,19 +276,6 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
old_metadata = own_metadata(existing_item)
old_content = existing_item.get_explicitly_set_fields_by_scope(Scope.content)
if publish:
if publish == 'make_private':
try:
store.unpublish(existing_item.location, user.id),
except ItemNotFoundError:
pass
elif publish == 'create_draft':
try:
store.convert_to_draft(existing_item.location, user.id)
except DuplicateItemError:
pass
if data:
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
existing_item.data = data
@@ -555,8 +531,30 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
)
# Note that children aren't being returned until we have a use case.
return {
'id': unicode(module.location),
'data': data,
'metadata': own_metadata(module)
return create_xblock_info(usage_key, module, data, own_metadata(module))
def create_xblock_info(usage_key, xblock, data=None, metadata=None):
"""
Creates the information needed for client-side XBlockInfo.
If data or metadata are not specified, their information will not be added
(regardless of whether or not the xblock actually has data or metadata).
"""
publish_state = compute_publish_state(xblock) if xblock else None
xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
"category": xblock.category,
"has_changes": modulestore().has_changes(usage_key),
"published": publish_state in (PublishState.public, PublishState.draft),
"edited_on": get_default_time_display(xblock.edited_on) if xblock.edited_on else None,
"edited_by": User.objects.get(id=xblock.edited_by).username if xblock.edited_by else None
}
if data is not None:
xblock_info["data"] = data
if metadata is not None:
xblock_info["metadata"] = metadata
return xblock_info

View File

@@ -549,22 +549,6 @@ class TestEditItem(ItemTest):
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
def test_make_private(self):
""" Test making a public problem private (un-publishing it). """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
# Now make it private
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_private'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.private)
def test_make_draft(self):
""" Test creating a draft version of a public problem. """
# Make problem public.
@@ -574,13 +558,6 @@ class TestEditItem(ItemTest):
)
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
# Now make it draft, which means both versions will exist.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'create_draft'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.draft)
# Update the draft version and check that published is different.
self.client.ajax_post(
self.problem_update_url,
@@ -589,6 +566,9 @@ class TestEditItem(ItemTest):
updated_draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
self.assertEqual(updated_draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
self.assertIsNone(published.due)
# Fetch the published version again to make sure the due date is still unset.
published = modulestore().get_item(published.location, revision=REVISION_OPTION_PUBLISHED_ONLY)
self.assertIsNone(published.due)
def test_make_public_with_update(self):
""" Update a problem and make it public at the same time. """
@@ -602,112 +582,6 @@ class TestEditItem(ItemTest):
published = self.get_item_from_modulestore(self.problem_usage_key)
self.assertEqual(published.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
def test_make_private_with_update(self):
""" Make a problem private and update it at the same time. """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
# Make problem private and update.
self.client.ajax_post(
self.problem_update_url,
data={
'metadata': {'due': '2077-10-10T04:00Z'},
'publish': 'make_private'
}
)
draft = self.verify_publish_state(self.problem_usage_key, PublishState.private)
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
def test_create_draft_with_update(self):
""" Create a draft and update it at the same time. """
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
# Now make it draft, which means both versions will exist.
self.client.ajax_post(
self.problem_update_url,
data={
'metadata': {'due': '2077-10-10T04:00Z'},
'publish': 'create_draft'
}
)
draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
self.assertEqual(draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
self.assertIsNone(published.due)
def test_create_draft_with_multiple_requests(self):
"""
Create a draft request returns already created version if it exists.
"""
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
# Now make it draft, which means both versions will exist.
self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'create_draft'
}
)
draft_1 = self.verify_publish_state(self.problem_usage_key, PublishState.draft)
# Now check that when a user sends request to create a draft when there is already a draft version then
# user gets that already created draft instead of getting 'DuplicateItemError' exception.
self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'create_draft'
}
)
draft_2 = self.verify_publish_state(self.problem_usage_key, PublishState.draft)
self.assertIsNotNone(draft_2)
self.assertEqual(draft_1, draft_2)
def test_make_private_with_multiple_requests(self):
"""
Make private requests gets proper response even if xmodule is already made private.
"""
# Make problem public.
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
self.assertIsNotNone(self.get_item_from_modulestore(self.problem_usage_key))
# Now make it private, and check that its version is private
resp = self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'make_private'
}
)
self.assertEqual(resp.status_code, 200)
draft_1 = self.verify_publish_state(self.problem_usage_key, PublishState.private)
# Now check that when a user sends request to make it private when it already is private then
# user gets that private version instead of getting 'ItemNotFoundError' exception.
self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'make_private'
}
)
self.assertEqual(resp.status_code, 200)
draft_2 = self.verify_publish_state(self.problem_usage_key, PublishState.private)
self.assertEqual(draft_1, draft_2)
def test_published_and_draft_contents_with_update(self):
""" Create a draft and publish it then modify the draft and check that published content is not modified """
@@ -724,8 +598,7 @@ class TestEditItem(ItemTest):
data={
'id': unicode(self.problem_usage_key),
'metadata': {},
'data': "<p>Problem content draft.</p>",
'publish': 'create_draft'
'data': "<p>Problem content draft.</p>"
}
)
@@ -746,6 +619,9 @@ class TestEditItem(ItemTest):
# Both published and draft content should still be different
draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
self.assertNotEqual(draft.data, published.data)
# Fetch the published version again to make sure the data is correct.
published = modulestore().get_item(published.location, revision=REVISION_OPTION_PUBLISHED_ONLY)
self.assertNotEqual(draft.data, published.data)
def test_publish_states_of_nested_xblocks(self):
""" Test publishing of a unit page containing a nested xblock """
@@ -777,7 +653,6 @@ class TestEditItem(ItemTest):
data={
'id': unicode(unit_usage_key),
'metadata': {},
'publish': 'create_draft'
}
)
self.assertEqual(resp.status_code, 200)