-
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
[FC-0009] Copy/Paste associated static assets #32346
[FC-0009] Copy/Paste associated static assets #32346
Conversation
Thanks for the pull request, @bradenmacdonald! As a core committer in this repo, you can merge this once the pull request is approved per the core committer reviewer requirements and according to the agreement with your edX Champion. |
@Agrendalath @ormsbee FYI, this is still WIP but you can have a look at how it's shaping up if you want. When copying an XBlock, you can see its static assets staged in the django admin, but I haven't implemented pasting yet. |
84644d3
to
c4ce948
Compare
0734670
to
3b87039
Compare
3b87039
to
63d152d
Compare
@bradenmacdonald, I'll review this tomorrow. |
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.
@bradenmacdonald, great work! I left some comments about edge cases, but other than that, it works as expected.
@Agrendalath Thank you for the really excellent review. I've addressed all your comments. |
b0c5dab
to
bb30799
Compare
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.
👍
- I tested this: copying various XBlocks works correctly in different cases (missing files, missing data, large files, duplicates, file conflicts, transcripts, etc.)
- I read through the code
- I checked for accessibility issues: n/a
- Includes documentation: n/a
- I made sure any change in configuration variables is reflected in the corresponding client's
configuration-secure
repository: n/a
# and then either inform the user or find another way to import the file (e.g. if the file still | ||
# exists in the "Files & Uploads" contentstore of the source course, based on source_key_str). | ||
data_file=ContentFile(content=f.data, name=f.filename) if f.data else None, | ||
source_key_str=str(source_key) if source_key else "", |
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.
source_key_str=str(source_key) if source_key else "", | |
source_key_str=str(f.source_key) if f.source_key else "", |
# Enqueue a (potentially slow) task to delete the old staged content | ||
try: | ||
delete_expired_clipboards.delay(expired_ids) | ||
except Exception as err: # pylint: disable=broad-except | ||
log.exception(f"Unable to enqueue cleanup task for StagedContents: {','.join(str(x) for x in expired_ids)}") | ||
# Return the response: | ||
return Response(serializer.data) | ||
|
||
def _save_static_assets_to_clipboard(self, static_files, usage_key, staged_content): |
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.
def _save_static_assets_to_clipboard(self, static_files, usage_key, staged_content): | |
@staticmethod | |
def _save_static_assets_to_clipboard( | |
static_files: list[StaticFile], usage_key: UsageKey, staged_content: StagedContent | |
): |
Just a suggestion. We'll need from openedx.core.lib.xblock_serializer.api import serialize_xblock_to_olx, StaticFile
to get this working.
source_key_str=str(source_key) if source_key else "", | ||
md5_hash=f.md5_hash or "", | ||
) | ||
except Exception as err: # pylint: disable=broad-except |
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.
Tiny nit: we're not using the err
variable in any of these except
clauses in this file (you can grep by "as err").
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.
Huh, I thought pylint would have warned about that. Thanks.
if data is None: | ||
raise NotFoundError(file_data_obj.source_key) | ||
content = StaticContent(new_key, name=filename, content_type=content_type, data=data) | ||
contentstore().save(content) |
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.
contentstore().save(content) | |
thumbnail_content, thumbnail_location = contentstore().generate_thumbnail(content) | |
if thumbnail_content is not None: | |
content.thumbnail_location = thumbnail_location | |
contentstore().save(content) |
Optional. Currently, the thumbnails for copied files are missing.
if content: | ||
md5_hash = hashlib.md5(f.data).hexdigest() | ||
# This asset came from the XBlock's filesystem, e.g. a video block's transcript file | ||
source_key = usage_key | ||
elif source_key: | ||
sc = contentstore().find(source_key) | ||
md5_hash = sc.content_digest | ||
content = sc.data | ||
else: | ||
md5_hash = "" # Unknown |
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.
if content: | |
md5_hash = hashlib.md5(f.data).hexdigest() | |
# This asset came from the XBlock's filesystem, e.g. a video block's transcript file | |
source_key = usage_key | |
elif source_key: | |
sc = contentstore().find(source_key) | |
md5_hash = sc.content_digest | |
content = sc.data | |
else: | |
md5_hash = "" # Unknown | |
md5_hash = "" | |
if content: | |
md5_hash = hashlib.md5(f.data).hexdigest() | |
# This asset came from the XBlock's filesystem, e.g. a video block's transcript file | |
source_key = usage_key | |
# Check if the resource exists. It can be absent if an XBlock contains an invalid link. | |
elif source_key and (sc := AssetManager.find(source_key, throw_on_not_found=False)): | |
md5_hash = sc.content_digest | |
content = sc.data |
To get this snippet working, we need to replace from xmodule.contentstore.django import contentstore
with from xmodule.assetstore.assetmgr import AssetManager
.
This fixes the scenario when /static/absent_file
is present in the XBlock. Otherwise, the copy operation is interrupted.
@ormsbee, this is ready for your review. |
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.
Just comments/questions–nothing to block merge.
# However, currently the only known case for that is video block's transcript files, and those will | ||
# automatically be "carried over" to the new XBlock even in a different course because the video ID is the same, | ||
# and VAL will thus make the transcript available. |
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.
Does this work with VideoBlocks that don't use edx-val, i.e. those with .srt
files that were manually uploaded? The VideoBlock has its own weird logic to do those file mappings, right?
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.
FWIW, I'm okay with merging this even if it doesn't work quite right for non-pipeline video, but I want to make sure I'm understanding things correctly.
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.
Ah, yes. The video block's .srt files are copied into the clipboard in any case, since the logic was already in the serializer, but when I was testing things I couldn't find any use case to actually make them available at paste time. So I just added this comment and left it at that for now. I guess that if VAL is disabled, then this would be useful. Do you think that's something important to support in the future?
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.
I do think so, yeah. Many folks won't use the video pipeline because they prefer to manage their videos through other services, and the new VideoBlock work is going to preserve support for different sources.
But like I said, not a blocker.
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.
This API is considered BETA. Once it is stable, this definition should be moved into openedx_filters. | ||
""" | ||
|
||
filter_type = "org.openedx.content_authoring.staged_content.static_filter_source.v1" |
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.
Not blocking, but I'm curious why this was done as a filter, as opposed to some new optional method on XBlocks?
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.
Mostly I wanted this to be very flexible and configurable at any level - the instance level in particular. If some XBlock has a need to associate a particular static file with itself, it can do so using a filter, but so can an instance administrator if there is some usage pattern on their instance that requires static files for, say, HTML XBlocks, which the normal method of detection doesn't pick up. I consider this kind of an experimental approach though as it says in the docstring. If it doesn't see any usage we can remove the filter entirely.
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.
On second thought, now that I'm not using filters for python_lib.zip I think I'll do a follow-up PR to just remove this filter. YAGNI. openedx/modular-learning#70
""" | ||
Filter the list of file_datas to remove any large files | ||
""" | ||
limit = 10 * 1024 * 1024 |
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.
Is there a specific set of files we're expecting to get filtered at this point?
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.
No, I just wanted to avoid performance issues if someone copies an XBlock that links to a huge file that's in Files & Uploads. Don't want to block the copy from completion while huge files are uploaded to temp storage that may not even be needed.
So in such a case (copying something that references a huge file), now we'll just skip that file, and intra-course copy-paste will still work for, and for cross-course copy-paste the user can copy the large asset manually.
Co-authored-by: David Ormsbee <[email protected]>
@bradenmacdonald 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
2U Release Notice: This PR has been deployed to the edX production environment. |
Description
This implements openedx/modular-learning#16 so that users can copy and paste components between different courses, with static assets coming along "automatically" as needed.
Supporting information
See openedx/modular-learning#16
This also includes a small fix for openedx/modular-learning#66
Testing instructions
Prequisite: enable the new copy-paste feature:
contentstore.enable_copy_paste_feature
and enable it for EveryoneInstructions:
You need two courses.
In course A, create some components that reference static assets using the
/static/blah.ext
format. The files should be added to course A's "Files & Uploads". Ideally:python_lib.zip
but doesn't directly reference it (example from MIT; though note that the samplecourse
contains two symlinks which must be replaced with hard links before it can be imported, if you want to import their sample course).Copy these components and paste them into Course B.
Verify that the static assets now appear in Course B's "Files & Uploads" and that the components work as before.
Also test what happens if Course B already has an existing file with the same name but different contents (a warning should be shown).
This also includes a fix for When copy-pasting a Text (HTML) block, the "Raw Editor" setting is reset modular-learning#66 - please set an HTML block to "raw" editor then copy and paste it, and ensure the raw editor is saved.
Known issue: in the destination course's "Files & Uploads" page, no thumbnail images will be shown for automatically-copied assets. I'm not sure why but can investigate that in a future PR.Working now!Deadline
None
TODOs
Display the list of assets in the paste preview(excluded from UX design for now)python_lib.zip
or at least store its hashPrivate-ref: MNG-3723