Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
0d8d7cb3
Commit
0d8d7cb3
authored
Jan 15, 2014
by
cahrens
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support duplicating an existing xblock to a supplied parent location.
STUD-1190
parent
e475f835
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
187 additions
and
8 deletions
+187
-8
cms/djangoapps/contentstore/tests/test_item.py
+122
-3
cms/djangoapps/contentstore/views/item.py
+65
-5
No files found.
cms/djangoapps/contentstore/tests/test_item.py
View file @
0d8d7cb3
...
...
@@ -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.
...
...
cms/djangoapps/contentstore/views/item.py
View file @
0d8d7cb3
...
...
@@ -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.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment