New Publishing controls on Unit page.
STUD-1707
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user