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
52f8c976
Commit
52f8c976
authored
Dec 28, 2016
by
Mushtaq Ali
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move an item - TNL-6064
parent
3f355241
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
353 additions
and
7 deletions
+353
-7
cms/djangoapps/contentstore/views/item.py
+122
-6
cms/djangoapps/contentstore/views/tests/test_item.py
+231
-1
No files found.
cms/djangoapps/contentstore/views/item.py
View file @
52f8c976
...
@@ -19,6 +19,8 @@ from django.views.decorators.http import require_http_methods
...
@@ -19,6 +19,8 @@ from django.views.decorators.http import require_http_methods
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.locator
import
LibraryUsageLocator
from
opaque_keys.edx.locator
import
LibraryUsageLocator
from
pytz
import
UTC
from
pytz
import
UTC
from
xblock.core
import
XBlock
from
xblock.fields
import
Scope
from
xblock.fields
import
Scope
from
xblock.fragment
import
Fragment
from
xblock.fragment
import
Fragment
from
xblock_django.user_service
import
DjangoXBlockUserService
from
xblock_django.user_service
import
DjangoXBlockUserService
...
@@ -127,8 +129,11 @@ def xblock_handler(request, usage_key_string):
...
@@ -127,8 +129,11 @@ def xblock_handler(request, usage_key_string):
if usage_key_string is not specified, create a new xblock instance, either by duplicating
if usage_key_string 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
an existing xblock, or creating an entirely new one. The json playload can contain
these fields:
these fields:
:parent_locator: parent for new xblock, required for
both duplicat
e and create new instance
:parent_locator: parent for new xblock, required for
duplicate, mov
e and create new instance
:duplicate_source_locator: if present, use this as the source for creating a duplicate copy
:duplicate_source_locator: if present, use this as the source for creating a duplicate copy
:move_source_locator: if present, use this as the source item for moving
:target_index: if present, use this as the target index for moving an item to a particular index
otherwise target_index is calculated. It is sent back in the response.
:category: type of xblock, required if duplicate_source_locator is not present.
:category: type of xblock, required if duplicate_source_locator is not present.
:display_name: name for new xblock, optional
:display_name: name for new xblock, optional
:boilerplate: template name for populating fields, optional and only used
:boilerplate: template name for populating fields, optional and only used
...
@@ -198,14 +203,26 @@ def xblock_handler(request, usage_key_string):
...
@@ -198,14 +203,26 @@ def xblock_handler(request, usage_key_string):
request
.
user
,
request
.
user
,
request
.
json
.
get
(
'display_name'
),
request
.
json
.
get
(
'display_name'
),
)
)
return
JsonResponse
({
'locator'
:
unicode
(
dest_usage_key
),
'courseKey'
:
unicode
(
dest_usage_key
.
course_key
)})
return
JsonResponse
({
"locator"
:
unicode
(
dest_usage_key
),
"courseKey"
:
unicode
(
dest_usage_key
.
course_key
)})
else
:
else
:
return
_create_item
(
request
)
return
_create_item
(
request
)
elif
request
.
method
==
'PATCH'
:
if
'move_source_locator'
in
request
.
json
:
move_source_usage_key
=
usage_key_with_run
(
request
.
json
.
get
(
'move_source_locator'
))
target_parent_usage_key
=
usage_key_with_run
(
request
.
json
.
get
(
'parent_locator'
))
target_index
=
request
.
json
.
get
(
'target_index'
)
if
(
not
has_studio_write_access
(
request
.
user
,
target_parent_usage_key
.
course_key
)
or
not
has_studio_read_access
(
request
.
user
,
target_parent_usage_key
.
course_key
)
):
raise
PermissionDenied
()
return
_move_item
(
move_source_usage_key
,
target_parent_usage_key
,
request
.
user
,
target_index
)
return
JsonResponse
({
'error'
:
'Patch request did not recognise any parameters to handle.'
},
status
=
400
)
else
:
else
:
return
HttpResponseBadRequest
(
return
HttpResponseBadRequest
(
"Only instance creation is supported without a usage key."
,
'Only instance creation is supported without a usage key.'
,
content_type
=
"text/plain"
content_type
=
'text/plain'
)
)
...
@@ -636,10 +653,109 @@ def _create_item(request):
...
@@ -636,10 +653,109 @@ def _create_item(request):
)
)
return
JsonResponse
(
return
JsonResponse
(
{
"locator"
:
unicode
(
created_block
.
location
),
"courseKey"
:
unicode
(
created_block
.
location
.
course_key
)}
{
'locator'
:
unicode
(
created_block
.
location
),
'courseKey'
:
unicode
(
created_block
.
location
.
course_key
)}
)
)
def
_get_source_index
(
source_usage_key
,
source_parent
):
"""
Get source index position of the XBlock.
Arguments:
source_usage_key (BlockUsageLocator): Locator of source item.
source_parent (XBlock): A parent of the source XBlock.
Returns:
source_index (int): Index position of the xblock in a parent.
"""
try
:
source_index
=
source_parent
.
children
.
index
(
source_usage_key
)
return
source_index
except
ValueError
:
return
None
def
_move_item
(
source_usage_key
,
target_parent_usage_key
,
user
,
target_index
=
None
):
"""
Move an existing xblock as a child of the supplied target_parent_usage_key.
Arguments:
source_usage_key (BlockUsageLocator): Locator of source item.
target_parent_usage_key (BlockUsageLocator): Locator of target parent.
target_index (int): If provided, insert source item at provided index location in target_parent_usage_key item.
Returns:
JsonResponse: Information regarding move operation. It may contains error info if an invalid move operation
is performed.
"""
# Get the list of all component type XBlocks
component_types
=
sorted
(
set
(
name
for
name
,
class_
in
XBlock
.
load_classes
())
-
set
(
DIRECT_ONLY_CATEGORIES
))
store
=
modulestore
()
with
store
.
bulk_operations
(
source_usage_key
.
course_key
):
source_item
=
store
.
get_item
(
source_usage_key
)
source_parent
=
source_item
.
get_parent
()
target_parent
=
store
.
get_item
(
target_parent_usage_key
)
source_type
=
source_item
.
category
target_parent_type
=
target_parent
.
category
error
=
None
# Store actual/initial index of the source item. This would be sent back with response,
# so that with Undo operation, it would easier to move back item to it's original/old index.
source_index
=
_get_source_index
(
source_usage_key
,
source_parent
)
valid_move_type
=
{
'vertical'
:
source_type
if
source_type
in
component_types
else
'component'
,
'sequential'
:
'vertical'
,
'chapter'
:
'sequential'
,
}
if
valid_move_type
.
get
(
target_parent_type
,
''
)
!=
source_type
:
error
=
'You can not move {source_type} into {target_parent_type}.'
.
format
(
source_type
=
source_type
,
target_parent_type
=
target_parent_type
,
)
elif
source_parent
.
location
==
target_parent
.
location
:
error
=
'You can not move an item into the same parent.'
elif
source_index
is
None
:
error
=
'{source_usage_key} not found in {parent_usage_key}.'
.
format
(
source_usage_key
=
unicode
(
source_usage_key
),
parent_usage_key
=
unicode
(
source_parent
.
location
)
)
else
:
try
:
target_index
=
int
(
target_index
)
if
target_index
is
not
None
else
None
if
len
(
target_parent
.
children
)
<
target_index
:
error
=
'You can not move {source_usage_key} at an invalid index ({target_index}).'
.
format
(
source_usage_key
=
unicode
(
source_usage_key
),
target_index
=
target_index
)
except
ValueError
:
error
=
'You must provide target_index ({target_index}) as an integer.'
.
format
(
target_index
=
target_index
)
if
error
:
return
JsonResponse
({
'error'
:
error
},
status
=
400
)
# Remove reference from old parent.
source_parent
.
children
.
remove
(
source_item
.
location
)
store
.
update_item
(
source_parent
,
user
.
id
)
# When target_index is provided, insert xblock at target_index position, otherwise insert at the end.
insert_at
=
target_index
if
target_index
is
not
None
else
len
(
target_parent
.
children
)
# Add to new parent at particular location.
target_parent
.
children
.
insert
(
insert_at
,
source_item
.
location
)
store
.
update_item
(
target_parent
,
user
.
id
)
context
=
{
'move_source_locator'
:
unicode
(
source_usage_key
),
'parent_locator'
:
unicode
(
target_parent_usage_key
),
'source_index'
:
target_index
if
target_index
is
not
None
else
source_index
}
return
JsonResponse
(
context
)
def
_duplicate_item
(
parent_usage_key
,
duplicate_source_usage_key
,
user
,
display_name
=
None
,
is_child
=
False
):
def
_duplicate_item
(
parent_usage_key
,
duplicate_source_usage_key
,
user
,
display_name
=
None
,
is_child
=
False
):
"""
"""
Duplicate an existing xblock as a child of the supplied parent_usage_key.
Duplicate an existing xblock as a child of the supplied parent_usage_key.
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
52f8c976
...
@@ -14,13 +14,14 @@ from django.test.client import RequestFactory
...
@@ -14,13 +14,14 @@ from django.test.client import RequestFactory
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
contentstore.utils
import
reverse_usage_url
,
reverse_course_url
from
contentstore.utils
import
reverse_usage_url
,
reverse_course_url
from
opaque_keys
import
InvalidKeyError
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
openedx.core.djangoapps.self_paced.models
import
SelfPacedConfiguration
from
contentstore.views.component
import
(
from
contentstore.views.component
import
(
component_handler
,
get_component_templates
component_handler
,
get_component_templates
)
)
from
contentstore.views.item
import
(
from
contentstore.views.item
import
(
create_xblock_info
,
_get_module_info
,
ALWAYS
,
VisibilityState
,
_xblock_type_and_display_name
,
create_xblock_info
,
_get_
source_index
,
_get_
module_info
,
ALWAYS
,
VisibilityState
,
_xblock_type_and_display_name
,
add_container_page_publishing_info
add_container_page_publishing_info
)
)
from
contentstore.tests.utils
import
CourseTestCase
from
contentstore.tests.utils
import
CourseTestCase
...
@@ -734,6 +735,235 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
...
@@ -734,6 +735,235 @@ class TestDuplicateItem(ItemTest, DuplicateHelper):
verify_name
(
self
.
seq_usage_key
,
self
.
chapter_usage_key
,
"customized name"
,
display_name
=
"customized name"
)
verify_name
(
self
.
seq_usage_key
,
self
.
chapter_usage_key
,
"customized name"
,
display_name
=
"customized name"
)
class
TestMoveItem
(
ItemTest
):
"""
Tests for move item.
"""
def
setUp
(
self
):
"""
Creates the test course structure to build course outline tree.
"""
super
(
TestMoveItem
,
self
)
.
setUp
()
# Create a parent chapter
chap1
=
self
.
create_xblock
(
parent_usage_key
=
self
.
course
.
location
,
display_name
=
'chapter1'
,
category
=
'chapter'
)
self
.
chapter_usage_key
=
self
.
response_usage_key
(
chap1
)
chap2
=
self
.
create_xblock
(
parent_usage_key
=
self
.
course
.
location
,
display_name
=
'chapter2'
,
category
=
'chapter'
)
self
.
chapter2_usage_key
=
self
.
response_usage_key
(
chap2
)
# create a sequential
seq1
=
self
.
create_xblock
(
parent_usage_key
=
self
.
chapter_usage_key
,
display_name
=
'seq1'
,
category
=
'sequential'
)
self
.
seq_usage_key
=
self
.
response_usage_key
(
seq1
)
seq2
=
self
.
create_xblock
(
parent_usage_key
=
self
.
chapter_usage_key
,
display_name
=
'seq2'
,
category
=
'sequential'
)
self
.
seq2_usage_key
=
self
.
response_usage_key
(
seq2
)
# create a vertical
vert1
=
self
.
create_xblock
(
parent_usage_key
=
self
.
seq_usage_key
,
display_name
=
'vertical1'
,
category
=
'vertical'
)
self
.
vert_usage_key
=
self
.
response_usage_key
(
vert1
)
vert2
=
self
.
create_xblock
(
parent_usage_key
=
self
.
seq_usage_key
,
display_name
=
'vertical2'
,
category
=
'vertical'
)
self
.
vert2_usage_key
=
self
.
response_usage_key
(
vert2
)
# create problem and an html component
problem1
=
self
.
create_xblock
(
parent_usage_key
=
self
.
vert_usage_key
,
display_name
=
'problem1'
,
category
=
'problem'
)
self
.
problem_usage_key
=
self
.
response_usage_key
(
problem1
)
html1
=
self
.
create_xblock
(
parent_usage_key
=
self
.
vert_usage_key
,
display_name
=
'html1'
,
category
=
'html'
)
self
.
html_usage_key
=
self
.
response_usage_key
(
html1
)
def
_move_component
(
self
,
source_usage_key
,
target_usage_key
,
target_index
=
None
):
"""
Helper method to send move request and returns the response.
Arguments:
source_usage_key (BlockUsageLocator): Locator of source item.
target_usage_key (BlockUsageLocator): Locator of target parent.
target_index (int): If provided, insert source item at the provided index location in target_usage_key item.
Returns:
resp (JsonResponse): Response after the move operation is complete.
"""
data
=
{
'move_source_locator'
:
unicode
(
source_usage_key
),
'parent_locator'
:
unicode
(
target_usage_key
)
}
if
target_index
is
not
None
:
data
[
'target_index'
]
=
target_index
return
self
.
client
.
patch
(
reverse
(
'contentstore.views.xblock_handler'
),
json
.
dumps
(
data
),
content_type
=
'application/json'
)
def
assert_move_item
(
self
,
source_usage_key
,
target_usage_key
,
target_index
=
None
):
"""
Assert move component.
Arguments:
source_usage_key (BlockUsageLocator): Locator of source item.
target_usage_key (BlockUsageLocator): Locator of target parent.
target_index (int): If provided, insert source item at the provided index location in target_usage_key item.
"""
parent_loc
=
self
.
store
.
get_parent_location
(
source_usage_key
)
parent
=
self
.
get_item_from_modulestore
(
parent_loc
)
source_index
=
_get_source_index
(
source_usage_key
,
parent
)
expected_index
=
target_index
if
target_index
is
not
None
else
source_index
response
=
self
.
_move_component
(
source_usage_key
,
target_usage_key
,
target_index
)
self
.
assertEqual
(
response
.
status_code
,
200
)
response
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response
[
'move_source_locator'
],
unicode
(
source_usage_key
))
self
.
assertEqual
(
response
[
'parent_locator'
],
unicode
(
target_usage_key
))
self
.
assertEqual
(
response
[
'source_index'
],
expected_index
)
new_parent_loc
=
self
.
store
.
get_parent_location
(
source_usage_key
)
self
.
assertEqual
(
new_parent_loc
,
target_usage_key
)
self
.
assertNotEqual
(
parent_loc
,
new_parent_loc
)
def
test_move_component
(
self
):
"""
Test move component with different xblock types.
"""
for
source_usage_key
,
target_usage_key
in
[
(
self
.
html_usage_key
,
self
.
vert2_usage_key
),
(
self
.
vert_usage_key
,
self
.
seq2_usage_key
),
(
self
.
seq_usage_key
,
self
.
chapter2_usage_key
)
]:
self
.
assert_move_item
(
source_usage_key
,
target_usage_key
)
def
test_move_source_index
(
self
):
"""
Test moving an item to a particular index.
"""
parent
=
self
.
get_item_from_modulestore
(
self
.
vert_usage_key
)
children
=
parent
.
get_children
()
self
.
assertEqual
(
len
(
children
),
2
)
# Create a component within vert2.
resp
=
self
.
create_xblock
(
parent_usage_key
=
self
.
vert2_usage_key
,
display_name
=
'html2'
,
category
=
'html'
)
html2_usage_key
=
self
.
response_usage_key
(
resp
)
# Move html2_usage_key inside vert_usage_key at second position.
self
.
assert_move_item
(
html2_usage_key
,
self
.
vert_usage_key
,
1
)
parent
=
self
.
get_item_from_modulestore
(
self
.
vert_usage_key
)
children
=
parent
.
get_children
()
self
.
assertEqual
(
len
(
children
),
3
)
self
.
assertEqual
(
children
[
1
]
.
location
,
html2_usage_key
)
def
test_move_undo
(
self
):
"""
Test move a component and move it back (undo).
"""
# Get the initial index of the component
parent
=
self
.
get_item_from_modulestore
(
self
.
vert_usage_key
)
original_index
=
_get_source_index
(
self
.
html_usage_key
,
parent
)
# Move component and verify that response contains initial index
response
=
self
.
_move_component
(
self
.
html_usage_key
,
self
.
vert2_usage_key
)
response
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
original_index
,
response
[
'source_index'
])
# Verify that new parent has the moved component at the last index.
parent
=
self
.
get_item_from_modulestore
(
self
.
vert2_usage_key
)
self
.
assertEqual
(
self
.
html_usage_key
,
parent
.
children
[
-
1
])
# Verify original and new index is different now.
source_index
=
_get_source_index
(
self
.
html_usage_key
,
parent
)
self
.
assertNotEquals
(
original_index
,
source_index
)
# Undo Move to the original index, use the source index fetched from the response.
response
=
self
.
_move_component
(
self
.
html_usage_key
,
self
.
vert_usage_key
,
response
[
'source_index'
])
response
=
json
.
loads
(
response
.
content
)
self
.
assertEquals
(
original_index
,
response
[
'source_index'
])
def
test_move_large_target_index
(
self
):
"""
Test moving an item at a large index would generate an error message.
"""
parent
=
self
.
get_item_from_modulestore
(
self
.
vert2_usage_key
)
parent_children_length
=
len
(
parent
.
children
)
response
=
self
.
_move_component
(
self
.
html_usage_key
,
self
.
vert2_usage_key
,
parent_children_length
+
10
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
json
.
loads
(
response
.
content
)
expected_error
=
'You can not move {usage_key} at an invalid index ({target_index}).'
.
format
(
usage_key
=
self
.
html_usage_key
,
target_index
=
parent_children_length
+
10
)
self
.
assertEqual
(
expected_error
,
response
[
'error'
])
new_parent_loc
=
self
.
store
.
get_parent_location
(
self
.
html_usage_key
)
self
.
assertEqual
(
new_parent_loc
,
self
.
vert_usage_key
)
def
test_invalid_move
(
self
):
"""
Test invalid move.
"""
parent_loc
=
self
.
store
.
get_parent_location
(
self
.
chapter_usage_key
)
response
=
self
.
_move_component
(
self
.
chapter_usage_key
,
self
.
usage_key
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
json
.
loads
(
response
.
content
)
expected_error
=
'You can not move {source_type} into {target_type}.'
.
format
(
source_type
=
self
.
chapter_usage_key
.
block_type
,
target_type
=
self
.
usage_key
.
block_type
)
self
.
assertEqual
(
expected_error
,
response
[
'error'
])
new_parent_loc
=
self
.
store
.
get_parent_location
(
self
.
chapter_usage_key
)
self
.
assertEqual
(
new_parent_loc
,
parent_loc
)
def
test_move_current_parent
(
self
):
"""
Test that a component can not be moved to it's current parent.
"""
parent_loc
=
self
.
store
.
get_parent_location
(
self
.
html_usage_key
)
self
.
assertEqual
(
parent_loc
,
self
.
vert_usage_key
)
response
=
self
.
_move_component
(
self
.
html_usage_key
,
self
.
vert_usage_key
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response
[
'error'
],
'You can not move an item into the same parent.'
)
self
.
assertEqual
(
self
.
store
.
get_parent_location
(
self
.
html_usage_key
),
parent_loc
)
def
test_move_invalid_source_index
(
self
):
"""
Test moving an item to an invalid index.
"""
target_index
=
'test_index'
parent_loc
=
self
.
store
.
get_parent_location
(
self
.
html_usage_key
)
response
=
self
.
_move_component
(
self
.
html_usage_key
,
self
.
vert2_usage_key
,
target_index
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
json
.
loads
(
response
.
content
)
error
=
'You must provide target_index ({target_index}) as an integer.'
.
format
(
target_index
=
target_index
)
self
.
assertEqual
(
response
[
'error'
],
error
)
new_parent_loc
=
self
.
store
.
get_parent_location
(
self
.
html_usage_key
)
self
.
assertEqual
(
new_parent_loc
,
parent_loc
)
def
test_move_no_target_locator
(
self
):
"""
Test move an item without specifying the target location.
"""
data
=
{
'move_source_locator'
:
unicode
(
self
.
html_usage_key
)}
with
self
.
assertRaises
(
InvalidKeyError
):
self
.
client
.
patch
(
reverse
(
'contentstore.views.xblock_handler'
),
json
.
dumps
(
data
),
content_type
=
'application/json'
)
def
test_no_move_source_locator
(
self
):
"""
Test patch request without providing a move source locator.
"""
response
=
self
.
client
.
patch
(
reverse
(
'contentstore.views.xblock_handler'
)
)
self
.
assertEqual
(
response
.
status_code
,
400
)
response
=
json
.
loads
(
response
.
content
)
self
.
assertEqual
(
response
[
'error'
],
'Patch request did not recognise any parameters to handle.'
)
class
TestDuplicateItemWithAsides
(
ItemTest
,
DuplicateHelper
):
class
TestDuplicateItemWithAsides
(
ItemTest
,
DuplicateHelper
):
"""
"""
Test the duplicate method for blocks with asides.
Test the duplicate method for blocks with asides.
...
...
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