Skip to content
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

Merged
merged 11 commits into from
Jun 27, 2023

Conversation

bradenmacdonald
Copy link
Contributor

@bradenmacdonald bradenmacdonald commented Jun 2, 2023

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:

  1. Go to http://studio.local.overhang.io:8001/admin/waffle/flag/ (or devstack/sandbox equivalent)
  2. Add a new flag, contentstore.enable_copy_paste_feature and enable it for Everyone

Instructions:

  1. You need two courses.

  2. 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:

    • A Text/HTML component that embeds an image or links to a PDF.
    • A drag and drop component with a custom image.
    • A video with a custom transcript file uploaded via its settings.
    • A Problem component that depends on python_lib.zip but doesn't directly reference it (example from MIT; though note that the sample course contains two symlinks which must be replaced with hard links before it can be imported, if you want to import their sample course).
  3. Copy these components and paste them into Course B.

  4. Verify that the static assets now appear in Course B's "Files & Uploads" and that the components work as before.

  5. Also test what happens if Course B already has an existing file with the same name but different contents (a warning should be shown).

  6. 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

  • Implement copying of static assets from "Files & Uploads"
  • Implement copying of static assets from the per-XBlock filesystem (e.g. video transcripts)
  • Implement pluggable filter to control what gets copied
  • Implement filter to avoid copying very large assets
  • Save static assets into the new course on paste
  • Display a notification to the user about new files or conflicting files
  • Display the list of assets in the paste preview (excluded from UX design for now)
  • Implement copying of python_lib.zip or at least store its hash
  • Add more test cases

Private-ref: MNG-3723

@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core committer labels Jun 2, 2023
@openedx-webhooks
Copy link

openedx-webhooks commented Jun 2, 2023

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.

@bradenmacdonald bradenmacdonald marked this pull request as draft June 2, 2023 01:34
@bradenmacdonald
Copy link
Contributor Author

@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.
Screenshot 2023-06-01 at 6 35 38 PM

@bradenmacdonald bradenmacdonald added the FC Relates to an Axim Funded Contribution project label Jun 2, 2023
@bradenmacdonald bradenmacdonald force-pushed the copy-asset-dependencies branch 4 times, most recently from 84644d3 to c4ce948 Compare June 8, 2023 02:36
@Agrendalath
Copy link
Member

@bradenmacdonald, I'll review this tomorrow.

Copy link
Member

@Agrendalath Agrendalath left a 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.

cms/djangoapps/contentstore/helpers.py Outdated Show resolved Hide resolved
cms/djangoapps/contentstore/helpers.py Outdated Show resolved Hide resolved
cms/djangoapps/contentstore/helpers.py Outdated Show resolved Hide resolved
openedx/core/djangoapps/content_staging/views.py Outdated Show resolved Hide resolved
openedx/core/lib/xblock_serializer/utils.py Outdated Show resolved Hide resolved
openedx/core/lib/xblock_serializer/utils.py Outdated Show resolved Hide resolved
openedx/core/lib/xblock_serializer/utils.py Outdated Show resolved Hide resolved
openedx/core/djangoapps/content_staging/views.py Outdated Show resolved Hide resolved
openedx/core/djangoapps/content_staging/views.py Outdated Show resolved Hide resolved
openedx/core/djangoapps/content_staging/views.py Outdated Show resolved Hide resolved
@bradenmacdonald
Copy link
Contributor Author

@Agrendalath Thank you for the really excellent review. I've addressed all your comments.

Copy link
Member

@Agrendalath Agrendalath left a 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 "",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

@Agrendalath Agrendalath Jun 21, 2023

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").

Copy link
Contributor Author

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines 189 to 198
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
Copy link
Member

@Agrendalath Agrendalath Jun 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

@Agrendalath
Copy link
Member

@ormsbee, this is ready for your review.

Copy link
Contributor

@ormsbee ormsbee left a 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.

openedx/core/djangoapps/content_staging/data.py Outdated Show resolved Hide resolved
Comment on lines +266 to +268
# 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.
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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"
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

@bradenmacdonald bradenmacdonald Jun 27, 2023

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
Copy link
Contributor

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?

Copy link
Contributor Author

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 <dave@axim.org>
@bradenmacdonald bradenmacdonald merged commit 12a8d99 into openedx:master Jun 27, 2023
@bradenmacdonald bradenmacdonald deleted the copy-asset-dependencies branch June 27, 2023 19:06
@openedx-webhooks
Copy link

@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.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production.

@edx-pipeline-bot
Copy link
Contributor

2U Release Notice: This PR has been deployed to the edX production environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core committer FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants