-
Notifications
You must be signed in to change notification settings - Fork 4k
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
refactor: inheritable authoring mixin callbacks for editing & duplication #33756
Changes from 16 commits
536493f
5901e25
5c2dabb
3e9bb60
42c2037
b4a4c96
aaa26bf
116b013
c5cb3f8
6199588
7f6924f
adeec3a
cd78eb3
893ff58
8ad064c
e94fa52
3cb2769
5312b5e
9c85399
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 |
---|---|---|
|
@@ -45,7 +45,7 @@ | |
wrap_xblock_aside | ||
) | ||
|
||
from ..utils import get_visibility_partition_info, StudioPermissionsService | ||
from ..utils import StudioPermissionsService, get_visibility_partition_info | ||
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. from formatter |
||
from .access import get_user_role | ||
from .session_kv_store import SessionKeyValueStore | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,16 @@ | |
|
||
|
||
import logging | ||
from datetime import datetime, timezone | ||
from uuid import uuid4 | ||
|
||
from django.conf import settings | ||
from web_fragments.fragment import Fragment | ||
from xblock.core import XBlock, XBlockMixin | ||
from xblock.fields import String, Scope | ||
from openedx_events.content_authoring.data import DuplicatedXBlockData | ||
from openedx_events.content_authoring.signals import XBLOCK_DUPLICATED | ||
|
||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
@@ -51,3 +56,111 @@ def visibility_view(self, _context=None): | |
scope=Scope.settings, | ||
enforce_type=True, | ||
) | ||
|
||
def editor_saved(self, user, old_metadata, old_content) -> None: # pylint: disable=unused-argument | ||
""" | ||
Called right *before* the block is written to the DB. Can be used, e.g., to modify fields before saving. | ||
|
||
By default, is a no-op. Can be overriden in subclasses. | ||
""" | ||
|
||
def post_editor_saved(self, user, old_metadata, old_content) -> None: # pylint: disable=unused-argument | ||
""" | ||
Called right *after* the block is written to the DB. Can be used, e.g., to spin up followup tasks. | ||
|
||
By default, is a no-op. Can be overriden in subclasses. | ||
""" | ||
Agrendalath marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def studio_duplicate( | ||
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. This function feels very much like runtime code to me. What's the rationale for putting it into an XBlock mixin rather than the runtime? It seems like the goal here is to make And what about duplicating in v2 content libraries - are we going to have a NewRuntimeAuthoringMixin that implements I guess my assumption has been that methods on XBlocks or their mixins generally only interact with the core XBlock API, the runtime, and/or runtime services. Any code that's specific to modulestore should be abstracted behind a runtime method or a service. I know we haven't always followed this pattern consistently, but I thought we had been moving in that direction. 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. ^ Note: This is more a question than a request for changes. And probably @kdmccormick and @Agrendalath are the ones best positioned to answer :) 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.
Yeah, that makes sense to me. This PR is part of a bigger GitHub issue (#33640), which I haven't been tracking closely enough to answer this, though. @kdmccormick definitely has a better idea about the planned follow-ups. 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. Good point @bradenmacdonald . This does seem like runtime capability rather than a block capability. Until recently I didn't really grasp the distinction between those two :P @DanielVZ96 and @Agrendalath , when I wrote up the issue for this refactoring, I thought it'd be pretty straightfoward and self-contained. In reality, it seems like it's wrapped up in the complexities of Content Libraries V2 and copy-paste. I should have warned you folks of that when you originally opened your draft PR; that's my bad. I need to spend the next couple of work days sorting out our plan for duplication, copy-paste, and import for library_content blocks, and then I'll come back with a recommendation of what to do here. Thanks for your responsiveness and patience so far. |
||
self, | ||
parent_usage_key, | ||
duplicate_source_usage_key, | ||
user, | ||
store, | ||
dest_usage_key=None, | ||
display_name=None, | ||
shallow=False, | ||
is_child=False, | ||
): | ||
""" | ||
Duplicate an existing xblock as a child of the supplied parent_usage_key. You can | ||
optionally specify what usage key the new duplicate block will use via dest_usage_key. | ||
|
||
If shallow is True, does not copy children. | ||
""" | ||
from cms.djangoapps.contentstore.utils import gather_block_attributes, load_services_for_studio | ||
|
||
if not dest_usage_key: | ||
# Change the blockID to be unique. | ||
dest_usage_key = self.location.replace(name=uuid4().hex) | ||
|
||
category = dest_usage_key.block_type | ||
|
||
duplicate_metadata, asides_to_create = gather_block_attributes( | ||
self, | ||
display_name=display_name, | ||
is_child=is_child, | ||
) | ||
|
||
dest_block = store.create_item( | ||
user.id, | ||
dest_usage_key.course_key, | ||
dest_usage_key.block_type, | ||
block_id=dest_usage_key.block_id, | ||
definition_data=self.get_explicitly_set_fields_by_scope(Scope.content), | ||
metadata=duplicate_metadata, | ||
runtime=self.runtime, | ||
asides=asides_to_create, | ||
) | ||
|
||
# Allow an XBlock to do anything fancy it may need to when duplicated from another block. | ||
load_services_for_studio(self.runtime, user) | ||
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. Just a quick note: we replaced |
||
dest_block.studio_post_duplicate(self, store, user, shallow=shallow) | ||
# pylint: disable=protected-access | ||
if "detached" not in self.runtime.load_block_type(category)._class_tags: | ||
parent = store.get_item(parent_usage_key) | ||
# If source was already a child of the parent, add duplicate immediately afterward. | ||
# Otherwise, add child to end. | ||
if self.location in parent.children: | ||
source_index = parent.children.index(self.location) | ||
parent.children.insert(source_index + 1, dest_block.location) | ||
else: | ||
parent.children.append(dest_block.location) | ||
store.update_item(parent, user.id) | ||
|
||
# .. event_implemented_name: XBLOCK_DUPLICATED | ||
XBLOCK_DUPLICATED.send_event( | ||
time=datetime.now(timezone.utc), | ||
xblock_info=DuplicatedXBlockData( | ||
usage_key=dest_block.location, | ||
block_type=dest_block.location.block_type, | ||
source_usage_key=duplicate_source_usage_key, | ||
), | ||
) | ||
|
||
return dest_block.location | ||
|
||
def studio_post_duplicate( | ||
self, | ||
source_block, | ||
store, | ||
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. If we're changing the API of Passing 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. i agree, i really wanted to not inject the store dependency but this was the only way i figured i could keep the exact same behavior/code as before. The API is very recent and I doubt anyone uses it considering it's also internal. I'll wait until @kdmccormick comes back to see what we can do to improve this API. |
||
user, | ||
shallow: bool, | ||
) -> None: # pylint: disable=unused-argument | ||
""" | ||
Called when after a block is duplicated. Can be used, e.g., for special handling of child duplication. | ||
|
||
Children must always be handled. In case of inheritance it can be done by running this method with super(). | ||
|
||
By default, implements standard duplication logic. | ||
""" | ||
if not source_block.has_children or shallow: | ||
return | ||
|
||
self.children = self.children or [] | ||
for child in source_block.children: | ||
child_block = store.get_item(child) | ||
dupe = child_block.studio_duplicate(self.location, child, user=user, store=store, is_child=True) | ||
if dupe not in self.children: # studio_duplicate may add the child for us. | ||
self.children.append(dupe) | ||
store.update_item(self, user.id) |
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.
it's an indirect import, should be going directly to contentstore.utils