-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix duplication of Randomized Content Blocks #7482
Changes from all commits
a6c0063
51905f0
ce19c76
fa95d32
bf414cf
7816345
8d8fa0f
5bf2c23
e95e5e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
from copy import copy | ||
from capa.responsetypes import registry | ||
from gettext import ngettext | ||
from lazy import lazy | ||
|
||
from .mako_module import MakoModuleDescriptor | ||
from opaque_keys.edx.locator import LibraryLocator | ||
|
@@ -269,6 +270,7 @@ def author_view(self, context): | |
'max_count': self.max_count, | ||
'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) | ||
# else: When shown on a unit page, don't show any sort of preview - | ||
# just the status of this block in the validation area. | ||
|
@@ -306,6 +308,25 @@ def non_editable_metadata_fields(self): | |
non_editable_fields.extend([LibraryContentFields.mode, LibraryContentFields.source_library_version]) | ||
return non_editable_fields | ||
|
||
@lazy | ||
def tools(self): | ||
""" | ||
Grab the library tools service or raise an error. | ||
""" | ||
return self.runtime.service(self, 'library_tools') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the block |
||
|
||
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 | ||
def refresh_children(self, request=None, suffix=None): # pylint: disable=unused-argument | ||
""" | ||
|
@@ -320,21 +341,50 @@ def refresh_children(self, request=None, suffix=None): # pylint: disable=unused | |
the version number of the libraries used, so we easily determine if | ||
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') | ||
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 | ||
lib_tools.update_children(self, user_id, user_perms) | ||
user_id = self.get_user_id() | ||
if not self.tools: | ||
return Response("Library Tools unavailable in current runtime.", status=400) | ||
self.tools.update_children(self, user_id, user_perms) | ||
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): | ||
""" | ||
Validates library version | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
from django.core.exceptions import PermissionDenied | ||
from opaque_keys.edx.locator import LibraryLocator | ||
from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE | ||
from xmodule.modulestore import ModuleStoreEnum | ||
from xmodule.modulestore.exceptions import ItemNotFoundError | ||
from xmodule.capa_module import CapaDescriptor | ||
|
||
|
@@ -21,14 +22,17 @@ def _get_library(self, library_key): | |
Given a library key like "library-v1:ProblemX+PR0B", return the | ||
'library' XBlock with meta-information about the library. | ||
|
||
A specific version may be specified. | ||
|
||
Returns None on error. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: You could use |
||
if not isinstance(library_key, LibraryLocator): | ||
library_key = LibraryLocator.from_string(library_key) | ||
assert library_key.version_guid is None | ||
|
||
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: | ||
return None | ||
|
||
|
@@ -102,7 +106,7 @@ def can_use_library_content(self, block): | |
""" | ||
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 | ||
references has been updated. It will re-fetch all matching blocks from | ||
|
@@ -123,6 +127,8 @@ def update_children(self, dest_block, user_id, user_perms=None): | |
|
||
source_blocks = [] | ||
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) | ||
if library is None: | ||
raise ValueError("Requested library not found.") | ||
|
@@ -138,7 +144,10 @@ def update_children(self, dest_block, user_id, user_perms=None): | |
with self.store.bulk_operations(dest_block.location.course_key): | ||
dest_block.source_library_version = unicode(library.location.library_key.version_guid) | ||
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 | ||
# but we must also set .children here to avoid overwriting the DB again | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: If you're giving XBlocks the ability to control duplication, perhaps it would be better to add the default code (which is currently in this method) as a global default in studio (by adding it to the StudioMixin), and then letting individual blocks override. That way, you don't have to check if the children are handled or not. This code can just always call
studio_post_duplicate
, and assume that the block is doing the right thing.