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
5795ba3f
Commit
5795ba3f
authored
Apr 03, 2015
by
Kelketek
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #7482 from open-craft/fix_duplicate_rcb
Fix duplication of Randomized Content Blocks
parents
e469e4da
e95e5e70
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
232 additions
and
50 deletions
+232
-50
cms/djangoapps/contentstore/tests/test_libraries.py
+61
-2
cms/djangoapps/contentstore/views/item.py
+11
-2
cms/djangoapps/contentstore/views/preview.py
+2
-2
cms/templates/studio_xblock_wrapper.html
+15
-10
common/lib/xmodule/xmodule/library_content_module.py
+62
-12
common/lib/xmodule/xmodule/library_root_xblock.py
+1
-0
common/lib/xmodule/xmodule/library_tools.py
+13
-4
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+25
-16
common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py
+9
-2
common/lib/xmodule/xmodule/studio_editable.py
+1
-0
common/test/acceptance/pages/studio/container.py
+14
-0
common/test/acceptance/tests/studio/test_studio_library_container.py
+18
-0
No files found.
cms/djangoapps/contentstore/tests/test_libraries.py
View file @
5795ba3f
...
@@ -3,6 +3,7 @@ Content library unit tests that require the CMS runtime.
...
@@ -3,6 +3,7 @@ Content library unit tests that require the CMS runtime.
"""
"""
from
contentstore.tests.utils
import
AjaxEnabledTestClient
,
parse_json
from
contentstore.tests.utils
import
AjaxEnabledTestClient
,
parse_json
from
contentstore.utils
import
reverse_url
,
reverse_usage_url
,
reverse_library_url
from
contentstore.utils
import
reverse_url
,
reverse_usage_url
,
reverse_library_url
from
contentstore.views.item
import
_duplicate_item
from
contentstore.views.preview
import
_load_preview_module
from
contentstore.views.preview
import
_load_preview_module
from
contentstore.views.tests.test_library
import
LIBRARY_REST_URL
from
contentstore.views.tests.test_library
import
LIBRARY_REST_URL
import
ddt
import
ddt
...
@@ -726,6 +727,7 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
...
@@ -726,6 +727,7 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
self
.
assertEqual
(
len
(
lc_block
.
children
),
1
if
expected_result
else
0
)
self
.
assertEqual
(
len
(
lc_block
.
children
),
1
if
expected_result
else
0
)
@ddt.ddt
class
TestOverrides
(
LibraryTestCase
):
class
TestOverrides
(
LibraryTestCase
):
"""
"""
Test that overriding block Scope.settings fields from a library in a specific course works
Test that overriding block Scope.settings fields from a library in a specific course works
...
@@ -745,6 +747,9 @@ class TestOverrides(LibraryTestCase):
...
@@ -745,6 +747,9 @@ class TestOverrides(LibraryTestCase):
publish_item
=
False
,
publish_item
=
False
,
)
)
# Refresh library now that we've added something.
self
.
library
=
modulestore
()
.
get_library
(
self
.
lib_key
)
# Also create a course:
# Also create a course:
with
modulestore
()
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
with
modulestore
()
.
default_store
(
ModuleStoreEnum
.
Type
.
split
):
self
.
course
=
CourseFactory
.
create
()
self
.
course
=
CourseFactory
.
create
()
...
@@ -822,7 +827,8 @@ class TestOverrides(LibraryTestCase):
...
@@ -822,7 +827,8 @@ class TestOverrides(LibraryTestCase):
self
.
assertEqual
(
self
.
problem
.
definition_locator
.
definition_id
,
definition_id
)
self
.
assertEqual
(
self
.
problem
.
definition_locator
.
definition_id
,
definition_id
)
self
.
assertEqual
(
self
.
problem_in_course
.
definition_locator
.
definition_id
,
definition_id
)
self
.
assertEqual
(
self
.
problem_in_course
.
definition_locator
.
definition_id
,
definition_id
)
def
test_persistent_overrides
(
self
):
@ddt.data
(
False
,
True
)
def
test_persistent_overrides
(
self
,
duplicate
):
"""
"""
Test that when we override Scope.settings values in a course,
Test that when we override Scope.settings values in a course,
the override values persist even when the block is refreshed
the override values persist even when the block is refreshed
...
@@ -834,7 +840,14 @@ class TestOverrides(LibraryTestCase):
...
@@ -834,7 +840,14 @@ class TestOverrides(LibraryTestCase):
self
.
problem_in_course
.
weight
=
new_weight
self
.
problem_in_course
.
weight
=
new_weight
modulestore
()
.
update_item
(
self
.
problem_in_course
,
self
.
user
.
id
)
modulestore
()
.
update_item
(
self
.
problem_in_course
,
self
.
user
.
id
)
self
.
problem_in_course
=
modulestore
()
.
get_item
(
self
.
problem_in_course
.
location
)
if
duplicate
:
# Check that this also works when the RCB is duplicated.
self
.
lc_block
=
modulestore
()
.
get_item
(
_duplicate_item
(
self
.
course
.
location
,
self
.
lc_block
.
location
,
self
.
user
)
)
self
.
problem_in_course
=
modulestore
()
.
get_item
(
self
.
lc_block
.
children
[
0
])
else
:
self
.
problem_in_course
=
modulestore
()
.
get_item
(
self
.
problem_in_course
.
location
)
self
.
assertEqual
(
self
.
problem_in_course
.
display_name
,
new_display_name
)
self
.
assertEqual
(
self
.
problem_in_course
.
display_name
,
new_display_name
)
self
.
assertEqual
(
self
.
problem_in_course
.
weight
,
new_weight
)
self
.
assertEqual
(
self
.
problem_in_course
.
weight
,
new_weight
)
...
@@ -852,6 +865,52 @@ class TestOverrides(LibraryTestCase):
...
@@ -852,6 +865,52 @@ class TestOverrides(LibraryTestCase):
self
.
assertEqual
(
self
.
problem_in_course
.
weight
,
new_weight
)
self
.
assertEqual
(
self
.
problem_in_course
.
weight
,
new_weight
)
self
.
assertEqual
(
self
.
problem_in_course
.
data
,
new_data_value
)
self
.
assertEqual
(
self
.
problem_in_course
.
data
,
new_data_value
)
def
test_duplicated_version
(
self
):
"""
Test that if a library is updated, and the content block is duplicated,
the new block will use the old library version and not the new one.
"""
store
=
modulestore
()
self
.
assertEqual
(
len
(
self
.
library
.
children
),
1
)
self
.
assertEqual
(
len
(
self
.
lc_block
.
children
),
1
)
# Edit the only problem in the library:
self
.
problem
.
display_name
=
"--changed in library--"
store
.
update_item
(
self
.
problem
,
self
.
user
.
id
)
# Create an additional problem block in the library:
ItemFactory
.
create
(
category
=
"problem"
,
parent_location
=
self
.
library
.
location
,
user_id
=
self
.
user
.
id
,
publish_item
=
False
,
)
# Refresh our reference to the library
self
.
library
=
store
.
get_library
(
self
.
lib_key
)
# Refresh our reference to the block
self
.
lc_block
=
store
.
get_item
(
self
.
lc_block
.
location
)
self
.
problem_in_course
=
store
.
get_item
(
self
.
problem_in_course
.
location
)
# The library has changed...
self
.
assertEqual
(
len
(
self
.
library
.
children
),
2
)
# But the block hasn't.
self
.
assertEqual
(
len
(
self
.
lc_block
.
children
),
1
)
self
.
assertEqual
(
self
.
problem_in_course
.
location
,
self
.
lc_block
.
children
[
0
])
self
.
assertEqual
(
self
.
problem_in_course
.
display_name
,
self
.
original_display_name
)
# Duplicate self.lc_block:
duplicate
=
store
.
get_item
(
_duplicate_item
(
self
.
course
.
location
,
self
.
lc_block
.
location
,
self
.
user
)
)
# The duplicate should have identical children to the original:
self
.
assertEqual
(
len
(
duplicate
.
children
),
1
)
self
.
assertTrue
(
self
.
lc_block
.
source_library_version
)
self
.
assertEqual
(
self
.
lc_block
.
source_library_version
,
duplicate
.
source_library_version
)
problem2_in_course
=
store
.
get_item
(
duplicate
.
children
[
0
])
self
.
assertEqual
(
problem2_in_course
.
display_name
,
self
.
original_display_name
)
class
TestIncompatibleModuleStore
(
LibraryTestCase
):
class
TestIncompatibleModuleStore
(
LibraryTestCase
):
"""
"""
...
...
cms/djangoapps/contentstore/views/item.py
View file @
5795ba3f
...
@@ -593,16 +593,25 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
...
@@ -593,16 +593,25 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
runtime
=
source_item
.
runtime
,
runtime
=
source_item
.
runtime
,
)
)
children_handled
=
False
if
hasattr
(
dest_module
,
'studio_post_duplicate'
):
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# let us know if we need to handle these or not.
children_handled
=
dest_module
.
studio_post_duplicate
(
store
,
source_item
)
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
# Because DAGs are not fully supported, we need to actually duplicate each child as well.
# Because DAGs are not fully supported, we need to actually duplicate each child as well.
if
source_item
.
has_children
:
if
source_item
.
has_children
and
not
children_handled
:
dest_module
.
children
=
[]
dest_module
.
children
=
dest_module
.
children
or
[]
for
child
in
source_item
.
children
:
for
child
in
source_item
.
children
:
dupe
=
_duplicate_item
(
dest_module
.
location
,
child
,
user
=
user
)
dupe
=
_duplicate_item
(
dest_module
.
location
,
child
,
user
=
user
)
if
dupe
not
in
dest_module
.
children
:
# _duplicate_item may add the child for us.
if
dupe
not
in
dest_module
.
children
:
# _duplicate_item may add the child for us.
dest_module
.
children
.
append
(
dupe
)
dest_module
.
children
.
append
(
dupe
)
store
.
update_item
(
dest_module
,
user
.
id
)
store
.
update_item
(
dest_module
,
user
.
id
)
# pylint: disable=protected-access
if
'detached'
not
in
source_item
.
runtime
.
load_block_type
(
category
)
.
_class_tags
:
if
'detached'
not
in
source_item
.
runtime
.
load_block_type
(
category
)
.
_class_tags
:
parent
=
store
.
get_item
(
parent_usage_key
)
parent
=
store
.
get_item
(
parent_usage_key
)
# If source was already a child of the parent, add duplicate immediately afterward.
# If source was already a child of the parent, add duplicate immediately afterward.
...
...
cms/djangoapps/contentstore/views/preview.py
View file @
5795ba3f
...
@@ -240,7 +240,6 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
...
@@ -240,7 +240,6 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
# Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
# Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now.
if
not
context
.
get
(
'is_pages_view'
,
None
)
and
view
in
PREVIEW_VIEWS
:
if
not
context
.
get
(
'is_pages_view'
,
None
)
and
view
in
PREVIEW_VIEWS
:
root_xblock
=
context
.
get
(
'root_xblock'
)
root_xblock
=
context
.
get
(
'root_xblock'
)
can_edit_visibility
=
not
isinstance
(
xblock
.
location
,
LibraryUsageLocator
)
is_root
=
root_xblock
and
xblock
.
location
==
root_xblock
.
location
is_root
=
root_xblock
and
xblock
.
location
==
root_xblock
.
location
is_reorderable
=
_is_xblock_reorderable
(
xblock
,
context
)
is_reorderable
=
_is_xblock_reorderable
(
xblock
,
context
)
template_context
=
{
template_context
=
{
...
@@ -251,7 +250,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
...
@@ -251,7 +250,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_root'
:
is_root
,
'is_root'
:
is_root
,
'is_reorderable'
:
is_reorderable
,
'is_reorderable'
:
is_reorderable
,
'can_edit'
:
context
.
get
(
'can_edit'
,
True
),
'can_edit'
:
context
.
get
(
'can_edit'
,
True
),
'can_edit_visibility'
:
can_edit_visibility
,
'can_edit_visibility'
:
context
.
get
(
'can_edit_visibility'
,
True
),
'can_add'
:
context
.
get
(
'can_add'
,
True
),
}
}
html
=
render_to_string
(
'studio_xblock_wrapper.html'
,
template_context
)
html
=
render_to_string
(
'studio_xblock_wrapper.html'
,
template_context
)
frag
=
wrap_fragment
(
frag
,
html
)
frag
=
wrap_fragment
(
frag
,
html
)
...
...
cms/templates/studio_xblock_wrapper.html
View file @
5795ba3f
...
@@ -80,19 +80,24 @@ messages = json.dumps(xblock.validate().to_json())
...
@@ -80,19 +80,24 @@ messages = json.dumps(xblock.validate().to_json())
</a>
</a>
</li>
</li>
% endif
% endif
<li
class=
"action-item action-duplicate"
>
% if can_add:
<a
href=
"#"
data-tooltip=
"${_("
Duplicate
")}"
class=
"duplicate-button action-button"
>
<li
class=
"action-item action-duplicate"
>
<i
class=
"icon fa fa-copy"
></i>
<a
href=
"#"
data-tooltip=
"${_("
Duplicate
")}"
class=
"duplicate-button action-button"
>
<span
class=
"sr"
>
${_("Duplicate")}
</span>
<i
class=
"icon fa fa-copy"
></i>
<span
class=
"sr"
>
${_("Duplicate")}
</span>
</a>
</li>
% endif
% endif
% if can_add:
<!-- If we can add, we can delete. -->
<li
class=
"action-item action-delete"
>
<a
href=
"#"
data-tooltip=
"${_("
Delete
")}"
class=
"delete-button action-button"
>
<i
class=
"icon fa fa-trash-o"
></i>
<span
class=
"sr"
>
${_("Delete")}
</span>
</a>
</a>
</li>
</li>
% endif
% endif
<li
class=
"action-item action-delete"
>
<a
href=
"#"
data-tooltip=
"${_("
Delete
")}"
class=
"delete-button action-button"
>
<i
class=
"icon fa fa-trash-o"
></i>
<span
class=
"sr"
>
${_("Delete")}
</span>
</a>
</li>
% if is_reorderable:
% if is_reorderable:
<li
class=
"action-item action-drag"
>
<li
class=
"action-item action-drag"
>
<span
data-tooltip=
"${_('Drag to reorder')}"
class=
"drag-handle action"
></span>
<span
data-tooltip=
"${_('Drag to reorder')}"
class=
"drag-handle action"
></span>
...
...
common/lib/xmodule/xmodule/library_content_module.py
View file @
5795ba3f
...
@@ -7,6 +7,7 @@ from lxml import etree
...
@@ -7,6 +7,7 @@ from lxml import etree
from
copy
import
copy
from
copy
import
copy
from
capa.responsetypes
import
registry
from
capa.responsetypes
import
registry
from
gettext
import
ngettext
from
gettext
import
ngettext
from
lazy
import
lazy
from
.mako_module
import
MakoModuleDescriptor
from
.mako_module
import
MakoModuleDescriptor
from
opaque_keys.edx.locator
import
LibraryLocator
from
opaque_keys.edx.locator
import
LibraryLocator
...
@@ -269,6 +270,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
...
@@ -269,6 +270,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
'max_count'
:
self
.
max_count
,
'max_count'
:
self
.
max_count
,
'display_name'
:
self
.
display_name
or
self
.
url_name
,
'display_name'
:
self
.
display_name
or
self
.
url_name
,
}))
}))
context
[
'can_edit_visibility'
]
=
False
self
.
render_children
(
context
,
fragment
,
can_reorder
=
False
,
can_add
=
False
)
self
.
render_children
(
context
,
fragment
,
can_reorder
=
False
,
can_add
=
False
)
# else: When shown on a unit page, don't show any sort of preview -
# else: When shown on a unit page, don't show any sort of preview -
# just the status of this block in the validation area.
# just the status of this block in the validation area.
...
@@ -306,6 +308,25 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
...
@@ -306,6 +308,25 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
non_editable_fields
.
extend
([
LibraryContentFields
.
mode
,
LibraryContentFields
.
source_library_version
])
non_editable_fields
.
extend
([
LibraryContentFields
.
mode
,
LibraryContentFields
.
source_library_version
])
return
non_editable_fields
return
non_editable_fields
@lazy
def
tools
(
self
):
"""
Grab the library tools service or raise an error.
"""
return
self
.
runtime
.
service
(
self
,
'library_tools'
)
def
get_user_id
(
self
):
"""
Get the ID of the current user.
"""
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
if
user_service
:
# May be None when creating bok choy test fixtures
user_id
=
user_service
.
get_current_user
()
.
opt_attrs
.
get
(
'edx-platform.user_id'
,
None
)
else
:
user_id
=
None
return
user_id
@XBlock.handler
@XBlock.handler
def
refresh_children
(
self
,
request
=
None
,
suffix
=
None
):
# pylint: disable=unused-argument
def
refresh_children
(
self
,
request
=
None
,
suffix
=
None
):
# pylint: disable=unused-argument
"""
"""
...
@@ -320,21 +341,50 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
...
@@ -320,21 +341,50 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
the version number of the libraries used, so we easily determine if
the version number of the libraries used, so we easily determine if
this block is up to date or not.
this block is up to date or not.
"""
"""
lib_tools
=
self
.
runtime
.
service
(
self
,
'library_tools'
)
if
not
lib_tools
:
# This error is diagnostic. The user won't see it, but it may be helpful
# during debugging.
return
Response
(
_
(
u"Course does not support Library tools."
),
status
=
400
)
user_service
=
self
.
runtime
.
service
(
self
,
'user'
)
user_perms
=
self
.
runtime
.
service
(
self
,
'studio_user_permissions'
)
user_perms
=
self
.
runtime
.
service
(
self
,
'studio_user_permissions'
)
if
user_service
:
user_id
=
self
.
get_user_id
()
# May be None when creating bok choy test fixtures
if
not
self
.
tools
:
user_id
=
user_service
.
get_current_user
()
.
opt_attrs
.
get
(
'edx-platform.user_id'
,
None
)
return
Response
(
"Library Tools unavailable in current runtime."
,
status
=
400
)
else
:
self
.
tools
.
update_children
(
self
,
user_id
,
user_perms
)
user_id
=
None
lib_tools
.
update_children
(
self
,
user_id
,
user_perms
)
return
Response
()
return
Response
()
# Copy over any overridden settings the course author may have applied to the blocks.
def
_copy_overrides
(
self
,
store
,
user_id
,
source
,
dest
):
"""
Copy any overrides the user has made on blocks in this library.
"""
for
field
in
source
.
fields
.
itervalues
():
if
field
.
scope
==
Scope
.
settings
and
field
.
is_set_on
(
source
):
setattr
(
dest
,
field
.
name
,
field
.
read_from
(
source
))
if
source
.
has_children
:
source_children
=
[
self
.
runtime
.
get_block
(
source_key
)
for
source_key
in
source
.
children
]
dest_children
=
[
self
.
runtime
.
get_block
(
dest_key
)
for
dest_key
in
dest
.
children
]
for
source_child
,
dest_child
in
zip
(
source_children
,
dest_children
):
self
.
_copy_overrides
(
store
,
user_id
,
source_child
,
dest_child
)
store
.
update_item
(
dest
,
user_id
)
def
studio_post_duplicate
(
self
,
store
,
source_block
):
"""
Used by the studio after basic duplication of a source block. We handle the children
ourselves, because we have to properly reference the library upstream and set the overrides.
Otherwise we'll end up losing data on the next refresh.
"""
# The first task will be to refresh our copy of the library to generate the children.
# We must do this at the currently set version of the library block. Otherwise we may not have
# exactly the same children-- someone may be duplicating an out of date block, after all.
user_id
=
self
.
get_user_id
()
user_perms
=
self
.
runtime
.
service
(
self
,
'studio_user_permissions'
)
# pylint: disable=no-member
if
not
self
.
tools
:
raise
RuntimeError
(
"Library tools unavailable, duplication will not be sane!"
)
self
.
tools
.
update_children
(
self
,
user_id
,
user_perms
,
version
=
self
.
source_library_version
)
self
.
_copy_overrides
(
store
,
user_id
,
source_block
,
self
)
# Children have been handled.
return
True
def
_validate_library_version
(
self
,
validation
,
lib_tools
,
version
,
library_key
):
def
_validate_library_version
(
self
,
validation
,
lib_tools
,
version
,
library_key
):
"""
"""
Validates library version
Validates library version
...
...
common/lib/xmodule/xmodule/library_root_xblock.py
View file @
5795ba3f
...
@@ -82,6 +82,7 @@ class LibraryRoot(XBlock):
...
@@ -82,6 +82,7 @@ class LibraryRoot(XBlock):
# Children must have a separate context from the library itself. Make a copy.
# Children must have a separate context from the library itself. Make a copy.
child_context
=
context
.
copy
()
child_context
=
context
.
copy
()
child_context
[
'show_preview'
]
=
self
.
show_children_previews
child_context
[
'show_preview'
]
=
self
.
show_children_previews
child_context
[
'can_edit_visibility'
]
=
False
child
=
self
.
runtime
.
get_block
(
child_key
)
child
=
self
.
runtime
.
get_block
(
child_key
)
child_view_name
=
StudioEditableModule
.
get_preview_view_name
(
child
)
child_view_name
=
StudioEditableModule
.
get_preview_view_name
(
child
)
...
...
common/lib/xmodule/xmodule/library_tools.py
View file @
5795ba3f
...
@@ -4,6 +4,7 @@ XBlock runtime services for LibraryContentModule
...
@@ -4,6 +4,7 @@ XBlock runtime services for LibraryContentModule
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
from
opaque_keys.edx.locator
import
LibraryLocator
from
opaque_keys.edx.locator
import
LibraryLocator
from
xmodule.library_content_module
import
ANY_CAPA_TYPE_VALUE
from
xmodule.library_content_module
import
ANY_CAPA_TYPE_VALUE
from
xmodule.modulestore
import
ModuleStoreEnum
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.capa_module
import
CapaDescriptor
from
xmodule.capa_module
import
CapaDescriptor
...
@@ -21,14 +22,17 @@ class LibraryToolsService(object):
...
@@ -21,14 +22,17 @@ class LibraryToolsService(object):
Given a library key like "library-v1:ProblemX+PR0B", return the
Given a library key like "library-v1:ProblemX+PR0B", return the
'library' XBlock with meta-information about the library.
'library' XBlock with meta-information about the library.
A specific version may be specified.
Returns None on error.
Returns None on error.
"""
"""
if
not
isinstance
(
library_key
,
LibraryLocator
):
if
not
isinstance
(
library_key
,
LibraryLocator
):
library_key
=
LibraryLocator
.
from_string
(
library_key
)
library_key
=
LibraryLocator
.
from_string
(
library_key
)
assert
library_key
.
version_guid
is
None
try
:
try
:
return
self
.
store
.
get_library
(
library_key
,
remove_version
=
False
,
remove_branch
=
False
)
return
self
.
store
.
get_library
(
library_key
,
remove_version
=
False
,
remove_branch
=
False
,
head_validation
=
False
)
except
ItemNotFoundError
:
except
ItemNotFoundError
:
return
None
return
None
...
@@ -102,7 +106,7 @@ class LibraryToolsService(object):
...
@@ -102,7 +106,7 @@ class LibraryToolsService(object):
"""
"""
return
self
.
store
.
check_supports
(
block
.
location
.
course_key
,
'copy_from_template'
)
return
self
.
store
.
check_supports
(
block
.
location
.
course_key
,
'copy_from_template'
)
def
update_children
(
self
,
dest_block
,
user_id
,
user_perms
=
None
):
def
update_children
(
self
,
dest_block
,
user_id
,
user_perms
=
None
,
version
=
None
):
"""
"""
This method is to be used when the library that a LibraryContentModule
This method is to be used when the library that a LibraryContentModule
references has been updated. It will re-fetch all matching blocks from
references has been updated. It will re-fetch all matching blocks from
...
@@ -123,6 +127,8 @@ class LibraryToolsService(object):
...
@@ -123,6 +127,8 @@ class LibraryToolsService(object):
source_blocks
=
[]
source_blocks
=
[]
library_key
=
dest_block
.
source_library_key
library_key
=
dest_block
.
source_library_key
if
version
:
library_key
=
library_key
.
replace
(
branch
=
ModuleStoreEnum
.
BranchName
.
library
,
version_guid
=
version
)
library
=
self
.
_get_library
(
library_key
)
library
=
self
.
_get_library
(
library_key
)
if
library
is
None
:
if
library
is
None
:
raise
ValueError
(
"Requested library not found."
)
raise
ValueError
(
"Requested library not found."
)
...
@@ -138,7 +144,10 @@ class LibraryToolsService(object):
...
@@ -138,7 +144,10 @@ class LibraryToolsService(object):
with
self
.
store
.
bulk_operations
(
dest_block
.
location
.
course_key
):
with
self
.
store
.
bulk_operations
(
dest_block
.
location
.
course_key
):
dest_block
.
source_library_version
=
unicode
(
library
.
location
.
library_key
.
version_guid
)
dest_block
.
source_library_version
=
unicode
(
library
.
location
.
library_key
.
version_guid
)
self
.
store
.
update_item
(
dest_block
,
user_id
)
self
.
store
.
update_item
(
dest_block
,
user_id
)
dest_block
.
children
=
self
.
store
.
copy_from_template
(
source_blocks
,
dest_block
.
location
,
user_id
)
head_validation
=
not
version
dest_block
.
children
=
self
.
store
.
copy_from_template
(
source_blocks
,
dest_block
.
location
,
user_id
,
head_validation
=
head_validation
)
# ^-- copy_from_template updates the children in the DB
# ^-- copy_from_template updates the children in the DB
# but we must also set .children here to avoid overwriting the DB again
# but we must also set .children here to avoid overwriting the DB again
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
View file @
5795ba3f
...
@@ -790,7 +790,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -790,7 +790,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
else
:
else
:
self
.
request_cache
.
data
[
'course_cache'
]
=
{}
self
.
request_cache
.
data
[
'course_cache'
]
=
{}
def
_lookup_course
(
self
,
course_key
):
def
_lookup_course
(
self
,
course_key
,
head_validation
=
True
):
"""
"""
Decode the locator into the right series of db access. Does not
Decode the locator into the right series of db access. Does not
return the CourseDescriptor! It returns the actual db json from
return the CourseDescriptor! It returns the actual db json from
...
@@ -799,11 +799,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -799,11 +799,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
Semantics: if course id and branch given, then it will get that branch. If
Semantics: if course id and branch given, then it will get that branch. If
also give a version_guid, it will see if the current head of that branch == that guid. If not
also give a version_guid, it will see if the current head of that branch == that guid. If not
it raises VersionConflictError (the version now differs from what it was when you got your
it raises VersionConflictError (the version now differs from what it was when you got your
reference)
reference) unless you specify head_validation = False, in which case it will return the
revision (if specified) by the course_key.
:param course_key: any subclass of CourseLocator
:param course_key: any subclass of CourseLocator
"""
"""
if
course_key
.
org
and
course_key
.
course
and
course_key
.
run
:
if
not
course_key
.
version_guid
:
head_validation
=
True
if
head_validation
and
course_key
.
org
and
course_key
.
course
and
course_key
.
run
:
if
course_key
.
branch
is
None
:
if
course_key
.
branch
is
None
:
raise
InsufficientSpecificationError
(
course_key
)
raise
InsufficientSpecificationError
(
course_key
)
...
@@ -937,11 +940,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -937,11 +940,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
"""
"""
return
CourseLocator
(
org
,
course
,
run
)
return
CourseLocator
(
org
,
course
,
run
)
def
_get_structure
(
self
,
structure_id
,
depth
,
**
kwargs
):
def
_get_structure
(
self
,
structure_id
,
depth
,
head_validation
=
True
,
**
kwargs
):
"""
"""
Gets Course or Library by locator
Gets Course or Library by locator
"""
"""
structure_entry
=
self
.
_lookup_course
(
structure_id
)
structure_entry
=
self
.
_lookup_course
(
structure_id
,
head_validation
=
head_validation
)
root
=
structure_entry
.
structure
[
'root'
]
root
=
structure_entry
.
structure
[
'root'
]
result
=
self
.
_load_items
(
structure_entry
,
[
root
],
depth
,
**
kwargs
)
result
=
self
.
_load_items
(
structure_entry
,
[
root
],
depth
,
**
kwargs
)
return
result
[
0
]
return
result
[
0
]
...
@@ -955,14 +958,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -955,14 +958,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
raise
ItemNotFoundError
(
course_id
)
raise
ItemNotFoundError
(
course_id
)
return
self
.
_get_structure
(
course_id
,
depth
,
**
kwargs
)
return
self
.
_get_structure
(
course_id
,
depth
,
**
kwargs
)
def
get_library
(
self
,
library_id
,
depth
=
0
,
**
kwargs
):
def
get_library
(
self
,
library_id
,
depth
=
0
,
head_validation
=
True
,
**
kwargs
):
"""
"""
Gets the 'library' root block for the library identified by the locator
Gets the 'library' root block for the library identified by the locator
"""
"""
if
not
isinstance
(
library_id
,
LibraryLocator
):
if
not
isinstance
(
library_id
,
LibraryLocator
):
# The supplied CourseKey is of the wrong type, so it can't possibly be stored in this modulestore.
# The supplied CourseKey is of the wrong type, so it can't possibly be stored in this modulestore.
raise
ItemNotFoundError
(
library_id
)
raise
ItemNotFoundError
(
library_id
)
return
self
.
_get_structure
(
library_id
,
depth
,
**
kwargs
)
return
self
.
_get_structure
(
library_id
,
depth
,
head_validation
=
head_validation
,
**
kwargs
)
def
has_course
(
self
,
course_id
,
ignore_case
=
False
,
**
kwargs
):
def
has_course
(
self
,
course_id
,
ignore_case
=
False
,
**
kwargs
):
"""
"""
...
@@ -2170,7 +2173,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2170,7 +2173,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
self
.
_update_head
(
destination_course
,
index_entry
,
destination_course
.
branch
,
destination_structure
[
'_id'
])
self
.
_update_head
(
destination_course
,
index_entry
,
destination_course
.
branch
,
destination_structure
[
'_id'
])
@contract
(
source_keys
=
"list(BlockUsageLocator)"
,
dest_usage
=
BlockUsageLocator
)
@contract
(
source_keys
=
"list(BlockUsageLocator)"
,
dest_usage
=
BlockUsageLocator
)
def
copy_from_template
(
self
,
source_keys
,
dest_usage
,
user_id
):
def
copy_from_template
(
self
,
source_keys
,
dest_usage
,
user_id
,
head_validation
=
True
):
"""
"""
Flexible mechanism for inheriting content from an external course/library/etc.
Flexible mechanism for inheriting content from an external course/library/etc.
...
@@ -2204,12 +2207,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2204,12 +2207,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# so that we can access descendant information quickly
# so that we can access descendant information quickly
source_structures
=
{}
source_structures
=
{}
for
key
in
source_keys
:
for
key
in
source_keys
:
course_key
=
key
.
course_key
.
for_version
(
None
)
course_key
=
key
.
course_key
if
course_key
.
branch
is
None
:
if
course_key
.
branch
is
None
:
raise
ItemNotFoundError
(
"branch is required for all source keys when using copy_from_template"
)
raise
ItemNotFoundError
(
"branch is required for all source keys when using copy_from_template"
)
if
course_key
not
in
source_structures
:
if
course_key
not
in
source_structures
:
with
self
.
bulk_operations
(
course_key
):
with
self
.
bulk_operations
(
course_key
):
source_structures
[
course_key
]
=
self
.
_lookup_course
(
course_key
)
.
structure
source_structures
[
course_key
]
=
self
.
_lookup_course
(
course_key
,
head_validation
=
head_validation
)
.
structure
destination_course
=
dest_usage
.
course_key
destination_course
=
dest_usage
.
course_key
with
self
.
bulk_operations
(
destination_course
):
with
self
.
bulk_operations
(
destination_course
):
...
@@ -2226,7 +2231,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2226,7 +2231,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# The descendants() method used above adds the block itself, which we don't consider a descendant.
# The descendants() method used above adds the block itself, which we don't consider a descendant.
orig_descendants
.
remove
(
block_key
)
orig_descendants
.
remove
(
block_key
)
new_descendants
=
self
.
_copy_from_template
(
new_descendants
=
self
.
_copy_from_template
(
source_structures
,
source_keys
,
dest_structure
,
block_key
,
user_id
source_structures
,
source_keys
,
dest_structure
,
block_key
,
user_id
,
head_validation
)
)
# Update the edit info:
# Update the edit info:
...
@@ -2250,7 +2255,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2250,7 +2255,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
for
k
in
dest_structure
[
'blocks'
][
block_key
]
.
fields
[
'children'
]
for
k
in
dest_structure
[
'blocks'
][
block_key
]
.
fields
[
'children'
]
]
]
def
_copy_from_template
(
self
,
source_structures
,
source_keys
,
dest_structure
,
new_parent_block_key
,
user_id
):
def
_copy_from_template
(
self
,
source_structures
,
source_keys
,
dest_structure
,
new_parent_block_key
,
user_id
,
head_validation
):
"""
"""
Internal recursive implementation of copy_from_template()
Internal recursive implementation of copy_from_template()
...
@@ -2263,9 +2270,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2263,9 +2270,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
new_children
=
list
()
# ordered list of the new children of new_parent_block_key
new_children
=
list
()
# ordered list of the new children of new_parent_block_key
for
usage_key
in
source_keys
:
for
usage_key
in
source_keys
:
src_course_key
=
usage_key
.
course_key
.
for_version
(
None
)
src_course_key
=
usage_key
.
course_key
hashable_source_id
=
src_course_key
.
for_version
(
None
)
block_key
=
BlockKey
(
usage_key
.
block_type
,
usage_key
.
block_id
)
block_key
=
BlockKey
(
usage_key
.
block_type
,
usage_key
.
block_id
)
source_structure
=
source_structures
.
get
(
src_course_key
,
[])
source_structure
=
source_structures
[
src_course_key
]
if
block_key
not
in
source_structure
[
'blocks'
]:
if
block_key
not
in
source_structure
[
'blocks'
]:
raise
ItemNotFoundError
(
usage_key
)
raise
ItemNotFoundError
(
usage_key
)
source_block_info
=
source_structure
[
'blocks'
][
block_key
]
source_block_info
=
source_structure
[
'blocks'
][
block_key
]
...
@@ -2273,7 +2282,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2273,7 +2282,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
# Compute a new block ID. This new block ID must be consistent when this
# Compute a new block ID. This new block ID must be consistent when this
# method is called with the same (source_key, dest_structure) pair
# method is called with the same (source_key, dest_structure) pair
unique_data
=
"{}:{}:{}"
.
format
(
unique_data
=
"{}:{}:{}"
.
format
(
unicode
(
src_course_key
)
.
encode
(
"utf-8"
),
unicode
(
hashable_source_id
)
.
encode
(
"utf-8"
),
block_key
.
id
,
block_key
.
id
,
new_parent_block_key
.
id
,
new_parent_block_key
.
id
,
)
)
...
@@ -2319,7 +2328,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
...
@@ -2319,7 +2328,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
if
children
:
if
children
:
children
=
[
src_course_key
.
make_usage_key
(
child
.
type
,
child
.
id
)
for
child
in
children
]
children
=
[
src_course_key
.
make_usage_key
(
child
.
type
,
child
.
id
)
for
child
in
children
]
new_blocks
|=
self
.
_copy_from_template
(
new_blocks
|=
self
.
_copy_from_template
(
source_structures
,
children
,
dest_structure
,
new_block_key
,
user_id
source_structures
,
children
,
dest_structure
,
new_block_key
,
user_id
,
head_validation
)
)
new_blocks
.
add
(
new_block_key
)
new_blocks
.
add
(
new_block_key
)
...
...
common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py
View file @
5795ba3f
...
@@ -58,7 +58,11 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
...
@@ -58,7 +58,11 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
course_id
=
self
.
_map_revision_to_branch
(
course_id
)
course_id
=
self
.
_map_revision_to_branch
(
course_id
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_course
(
course_id
,
depth
=
depth
,
**
kwargs
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_course
(
course_id
,
depth
=
depth
,
**
kwargs
)
def
get_library
(
self
,
library_id
,
depth
=
0
,
**
kwargs
):
def
get_library
(
self
,
library_id
,
depth
=
0
,
head_validation
=
True
,
**
kwargs
):
if
not
head_validation
and
library_id
.
version_guid
:
return
SplitMongoModuleStore
.
get_library
(
self
,
library_id
,
depth
=
depth
,
head_validation
=
head_validation
,
**
kwargs
)
library_id
=
self
.
_map_revision_to_branch
(
library_id
)
library_id
=
self
.
_map_revision_to_branch
(
library_id
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_library
(
library_id
,
depth
=
depth
,
**
kwargs
)
return
super
(
DraftVersioningModuleStore
,
self
)
.
get_library
(
library_id
,
depth
=
depth
,
**
kwargs
)
...
@@ -100,7 +104,10 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
...
@@ -100,7 +104,10 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
"""
"""
source_keys
=
[
self
.
_map_revision_to_branch
(
key
)
for
key
in
source_keys
]
source_keys
=
[
self
.
_map_revision_to_branch
(
key
)
for
key
in
source_keys
]
dest_key
=
self
.
_map_revision_to_branch
(
dest_key
)
dest_key
=
self
.
_map_revision_to_branch
(
dest_key
)
new_keys
=
super
(
DraftVersioningModuleStore
,
self
)
.
copy_from_template
(
source_keys
,
dest_key
,
user_id
)
head_validation
=
kwargs
.
get
(
'head_validation'
)
new_keys
=
super
(
DraftVersioningModuleStore
,
self
)
.
copy_from_template
(
source_keys
,
dest_key
,
user_id
,
head_validation
)
if
dest_key
.
branch
==
ModuleStoreEnum
.
BranchName
.
draft
:
if
dest_key
.
branch
==
ModuleStoreEnum
.
BranchName
.
draft
:
# Check if any of new_keys or their descendants need to be auto-published.
# Check if any of new_keys or their descendants need to be auto-published.
# We don't use _auto_publish_no_children since children may need to be published.
# We don't use _auto_publish_no_children since children may need to be published.
...
...
common/lib/xmodule/xmodule/studio_editable.py
View file @
5795ba3f
...
@@ -22,6 +22,7 @@ class StudioEditableBlock(object):
...
@@ -22,6 +22,7 @@ class StudioEditableBlock(object):
for
child
in
self
.
get_children
():
# pylint: disable=no-member
for
child
in
self
.
get_children
():
# pylint: disable=no-member
if
can_reorder
:
if
can_reorder
:
context
[
'reorderable_items'
]
.
add
(
child
.
location
)
context
[
'reorderable_items'
]
.
add
(
child
.
location
)
context
[
'can_add'
]
=
can_add
rendered_child
=
child
.
render
(
StudioEditableModule
.
get_preview_view_name
(
child
),
context
)
rendered_child
=
child
.
render
(
StudioEditableModule
.
get_preview_view_name
(
child
),
context
)
fragment
.
add_frag_resources
(
rendered_child
)
fragment
.
add_frag_resources
(
rendered_child
)
...
...
common/test/acceptance/pages/studio/container.py
View file @
5795ba3f
...
@@ -407,6 +407,20 @@ class XBlockWrapper(PageObject):
...
@@ -407,6 +407,20 @@ class XBlockWrapper(PageObject):
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'.wrapper-xblock.has-group-visibility-set'
))
.
is_present
()
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'.wrapper-xblock.has-group-visibility-set'
))
.
is_present
()
@property
@property
def
has_duplicate_button
(
self
):
"""
Returns true if this xblock has a 'duplicate' button
"""
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'a.duplicate-button'
))
@property
def
has_delete_button
(
self
):
"""
Returns true if this xblock has a 'delete' button
"""
return
self
.
q
(
css
=
self
.
_bounded_selector
(
'a.delete-button'
))
@property
def
has_edit_visibility_button
(
self
):
def
has_edit_visibility_button
(
self
):
"""
"""
Returns true if this xblock has an 'edit visibility' button
Returns true if this xblock has an 'edit visibility' button
...
...
common/test/acceptance/tests/studio/test_studio_library_container.py
View file @
5795ba3f
...
@@ -290,3 +290,21 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
...
@@ -290,3 +290,21 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest):
block
.
reset_field_val
(
"Display Name"
)
block
.
reset_field_val
(
"Display Name"
)
block
.
save_settings
()
block
.
save_settings
()
self
.
assertEqual
(
block
.
name
,
name_default
)
self
.
assertEqual
(
block
.
name
,
name_default
)
def
test_cannot_manage
(
self
):
"""
Scenario: Given I have a library, a course and library content xblock in a course
When I go to studio unit page for library content block
And when I click the "View" link
Then I can see a preview of the blocks drawn from the library.
And I do not see a duplicate button
And I do not see a delete button
"""
block_wrapper_unit_page
=
self
.
_get_library_xblock_wrapper
(
self
.
unit_page
.
xblocks
[
0
]
.
children
[
0
])
container_page
=
block_wrapper_unit_page
.
go_to_container
()
for
block
in
container_page
.
xblocks
:
self
.
assertFalse
(
block
.
has_duplicate_button
)
self
.
assertFalse
(
block
.
has_delete_button
)
self
.
assertFalse
(
block
.
has_edit_visibility_button
)
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