Support duplicating an existing xblock to a supplied parent location.
STUD-1190
This commit is contained in:
@@ -4,7 +4,7 @@ import json
|
||||
from datetime import datetime
|
||||
import ddt
|
||||
|
||||
from mock import Mock, patch
|
||||
from mock import patch
|
||||
from pytz import UTC
|
||||
from webob import Response
|
||||
|
||||
@@ -28,9 +28,10 @@ class ItemTest(CourseTestCase):
|
||||
def setUp(self):
|
||||
super(ItemTest, self).setUp()
|
||||
|
||||
self.unicode_locator = unicode(loc_mapper().translate_location(
|
||||
self.course_locator = loc_mapper().translate_location(
|
||||
self.course.location.course_id, self.course.location, False, True
|
||||
))
|
||||
)
|
||||
self.unicode_locator = unicode(self.course_locator)
|
||||
|
||||
def get_old_id(self, locator):
|
||||
"""
|
||||
@@ -157,6 +158,124 @@ class TestCreateItem(ItemTest):
|
||||
self.assertEqual(obj.start, datetime(2030, 1, 1, tzinfo=UTC))
|
||||
|
||||
|
||||
class TestDuplicateItem(ItemTest):
|
||||
"""
|
||||
Test the duplicate method.
|
||||
"""
|
||||
def setUp(self):
|
||||
""" Creates the test course structure and a few components to 'duplicate'. """
|
||||
super(TestDuplicateItem, self).setUp()
|
||||
# create a parent sequential
|
||||
resp = self.create_xblock(parent_locator=self.unicode_locator, category='sequential')
|
||||
self.seq_locator = self.response_locator(resp)
|
||||
|
||||
# create problem and an html component
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, category='problem', boilerplate='multiplechoice.yaml')
|
||||
self.problem_locator = self.response_locator(resp)
|
||||
|
||||
resp = self.create_xblock(parent_locator=self.seq_locator, category='html')
|
||||
self.html_locator = self.response_locator(resp)
|
||||
|
||||
def test_duplicate_equality(self):
|
||||
"""
|
||||
Tests that a duplicated xblock is identical to the original,
|
||||
except for location and display name.
|
||||
"""
|
||||
def verify_duplicate(source_locator, parent_locator):
|
||||
locator = self._duplicate_item(parent_locator, source_locator)
|
||||
original_item = self.get_item_from_modulestore(source_locator, draft=True)
|
||||
duplicated_item = self.get_item_from_modulestore(locator, draft=True)
|
||||
|
||||
self.assertNotEqual(
|
||||
original_item.location,
|
||||
duplicated_item.location,
|
||||
"Location of duplicate should be different from original"
|
||||
)
|
||||
# Set the location and display name to be the same so we can make sure the rest of the duplicate is equal.
|
||||
duplicated_item.location = original_item.location
|
||||
duplicated_item.display_name = original_item.display_name
|
||||
self.assertEqual(original_item, duplicated_item, "Duplicated item differs from original")
|
||||
|
||||
verify_duplicate(self.problem_locator, self.seq_locator)
|
||||
verify_duplicate(self.html_locator, self.seq_locator)
|
||||
verify_duplicate(self.seq_locator, self.unicode_locator)
|
||||
|
||||
def test_ordering(self):
|
||||
"""
|
||||
Tests the a duplicated xblock appears immediately after its source
|
||||
(if duplicate and source share the same parent), else at the
|
||||
end of the children of the parent.
|
||||
"""
|
||||
def verify_order(source_locator, parent_locator, source_position=None):
|
||||
locator = self._duplicate_item(parent_locator, source_locator)
|
||||
parent = self.get_item_from_modulestore(parent_locator)
|
||||
children = parent.children
|
||||
if source_position is None:
|
||||
self.assertFalse(source_locator in children, 'source item not expected in children array')
|
||||
self.assertEqual(
|
||||
children[len(children) - 1],
|
||||
self.get_old_id(locator).url(),
|
||||
"duplicated item not at end"
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
children[source_position],
|
||||
self.get_old_id(source_locator).url(),
|
||||
"source item at wrong position"
|
||||
)
|
||||
self.assertEqual(
|
||||
children[source_position+1],
|
||||
self.get_old_id(locator).url(),
|
||||
"duplicated item not ordered after source item"
|
||||
)
|
||||
|
||||
verify_order(self.problem_locator, self.seq_locator, 0)
|
||||
# 2 because duplicate of problem should be located before.
|
||||
verify_order(self.html_locator, self.seq_locator, 2)
|
||||
verify_order(self.seq_locator, self.unicode_locator, 0)
|
||||
|
||||
# Test duplicating something into a location that is not the parent of the original item.
|
||||
# Duplicated item should appear at the end.
|
||||
verify_order(self.html_locator, self.unicode_locator)
|
||||
|
||||
def test_display_name(self):
|
||||
"""
|
||||
Tests the expected display name for the duplicated xblock.
|
||||
"""
|
||||
def verify_name(source_locator, parent_locator, expected_name, display_name=None):
|
||||
locator = self._duplicate_item(parent_locator, source_locator, display_name)
|
||||
duplicated_item = self.get_item_from_modulestore(locator, draft=True)
|
||||
self.assertEqual(duplicated_item.display_name, expected_name)
|
||||
return locator
|
||||
|
||||
# Display name comes from template.
|
||||
dupe_locator = verify_name(self.problem_locator, self.seq_locator, "Duplicate of 'Multiple Choice'")
|
||||
# Test dupe of dupe.
|
||||
verify_name(dupe_locator, self.seq_locator, "Duplicate of 'Duplicate of 'Multiple Choice''")
|
||||
|
||||
# Uses default display_name of 'Text' from HTML component.
|
||||
verify_name(self.html_locator, self.seq_locator, "Duplicate of 'Text'")
|
||||
|
||||
# The sequence does not have a display_name set, so None gets included as the string 'None'.
|
||||
verify_name(self.seq_locator, self.unicode_locator, "Duplicate of 'None'")
|
||||
|
||||
# Now send a custom display name for the duplicate.
|
||||
verify_name(self.seq_locator, self.unicode_locator, "customized name", display_name="customized name")
|
||||
|
||||
def _duplicate_item(self, parent_locator, source_locator, display_name=None):
|
||||
data = {
|
||||
'parent_locator': parent_locator,
|
||||
'duplicate_source_locator': source_locator
|
||||
}
|
||||
if display_name is not None:
|
||||
data['display_name'] = display_name
|
||||
|
||||
resp = self.client.ajax_post('/xblock', json.dumps(data))
|
||||
resp_content = json.loads(resp.content)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
return resp_content['locator']
|
||||
|
||||
|
||||
class TestEditItem(ItemTest):
|
||||
"""
|
||||
Test xblock update.
|
||||
|
||||
@@ -33,6 +33,7 @@ from .helpers import _xmodule_recurse
|
||||
from preview import handler_prefix, get_preview_html
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
__all__ = ['orphan_handler', 'xblock_handler']
|
||||
|
||||
@@ -71,12 +72,15 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
:publish: can be one of three values, 'make_public, 'make_private', or 'create_draft'
|
||||
The JSON representation on the updated xblock (minus children) is returned.
|
||||
|
||||
if xblock locator is not specified, create a new xblock instance. The json playload can contain
|
||||
if xblock locator is not specified, create a new xblock instance, either by duplicating
|
||||
an existing xblock, or creating an entirely new one. The json playload can contain
|
||||
these fields:
|
||||
:parent_locator: parent for new xblock, required
|
||||
:category: type of xblock, required
|
||||
:parent_locator: parent for new xblock, required for both duplicate and create new instance
|
||||
:duplicate_source_locator: if present, use this as the source for creating a duplicate copy
|
||||
:category: type of xblock, required if duplicate_source_locator is not present.
|
||||
:display_name: name for new xblock, optional
|
||||
:boilerplate: template name for populating fields, optional
|
||||
:boilerplate: template name for populating fields, optional and only used
|
||||
if duplicate_source_locator is not present
|
||||
The locator (and old-style id) for the created xblock (minus children) is returned.
|
||||
"""
|
||||
if package_id is not None:
|
||||
@@ -131,7 +135,17 @@ def xblock_handler(request, tag=None, package_id=None, branch=None, version_guid
|
||||
publish=request.json.get('publish'),
|
||||
)
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
return _create_item(request)
|
||||
if 'duplicate_source_locator' in request.json:
|
||||
parent_locator = BlockUsageLocator(request.json['parent_locator'])
|
||||
duplicate_source_locator = BlockUsageLocator(request.json['duplicate_source_locator'])
|
||||
new_locator = _duplicate_item(
|
||||
parent_locator,
|
||||
duplicate_source_locator,
|
||||
request.json.get('display_name')
|
||||
)
|
||||
return JsonResponse({"locator": unicode(new_locator)})
|
||||
else:
|
||||
return _create_item(request)
|
||||
else:
|
||||
return HttpResponseBadRequest(
|
||||
"Only instance creation is supported without a package_id.",
|
||||
@@ -286,6 +300,52 @@ def _create_item(request):
|
||||
return JsonResponse({"locator": unicode(locator)})
|
||||
|
||||
|
||||
def _duplicate_item(parent_locator, duplicate_source_locator, display_name):
|
||||
"""
|
||||
Duplicate an existing xblock as a child of the supplied parent_locator.
|
||||
"""
|
||||
parent_location = loc_mapper().translate_locator_to_location(parent_locator)
|
||||
duplicate_source_location = loc_mapper().translate_locator_to_location(duplicate_source_locator)
|
||||
|
||||
store = get_modulestore(duplicate_source_location)
|
||||
source_item = store.get_item(duplicate_source_location)
|
||||
# Change the blockID to be unique.
|
||||
dest_location = duplicate_source_location.replace(name=uuid4().hex)
|
||||
category = dest_location.category
|
||||
|
||||
# Update the display name to indicate this is a duplicate (unless display name provided).
|
||||
duplicate_metadata = own_metadata(source_item)
|
||||
if display_name is not None:
|
||||
duplicate_metadata['display_name'] = display_name
|
||||
else:
|
||||
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
|
||||
|
||||
get_modulestore(category).create_and_save_xmodule(
|
||||
dest_location,
|
||||
definition_data=source_item.data if hasattr(source_item, 'data') else None,
|
||||
metadata=duplicate_metadata,
|
||||
system=source_item.system if hasattr(source_item, 'system') else None,
|
||||
)
|
||||
|
||||
# Children are not automatically copied over. Not all xblocks have a 'children' attribute.
|
||||
if hasattr(source_item, 'children'):
|
||||
get_modulestore(dest_location).update_children(dest_location, source_item.children)
|
||||
|
||||
if category not in DETACHED_CATEGORIES:
|
||||
parent = get_modulestore(parent_location).get_item(parent_location)
|
||||
# If source was already a child of the parent, add duplicate immediately afterward.
|
||||
# Otherwise, add child to end.
|
||||
if duplicate_source_location.url() in parent.children:
|
||||
source_index = parent.children.index(duplicate_source_location.url())
|
||||
parent.children.insert(source_index+1, dest_location.url())
|
||||
else:
|
||||
parent.children.append(dest_location.url())
|
||||
get_modulestore(parent_location).update_children(parent_location, parent.children)
|
||||
|
||||
course_location = loc_mapper().translate_locator_to_location(BlockUsageLocator(parent_locator), get_course=True)
|
||||
return loc_mapper().translate_location(course_location.course_id, dest_location, False, True)
|
||||
|
||||
|
||||
def _delete_item_at_location(item_location, delete_children=False, delete_all_versions=False):
|
||||
"""
|
||||
Deletes the item at with the given Location.
|
||||
|
||||
Reference in New Issue
Block a user