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

Import v2 libraries content into openedx-learning data models #33577

Closed
wants to merge 4 commits into from

Conversation

ormsbee
Copy link
Contributor

@ormsbee ormsbee commented Oct 24, 2023

Description

Initial code to move v2 libraries content into openedx-learning data models, via a new migrate_lib_to_learning_core management command in the content_libraries app. This will currently import XBlock definition OLX in both the published and draft branches from blockstore. It does not include associated assets at this time.

Blockers to Merging

I plan to merge this without associated asset support, which means it won't be ready to fully switch over. The idea I had was that we could convert a library for testing purposes and gradually build out the read/write portions of the XBlock runtime that has to interact with this.

Testing instructions

Run the following command in the Studio/CMS image:

python ./manage.py cms migrate_lib_to_learning_core -f "lib:Axim:200"

Where you replace lib:Axim:200 with the library key for an actual v2 content library on your machine. After that, you can explore the content you imported via the Django admin at http://studio.local.overhang.io:8001/admin/oel_publishing/publishableentity/

Screenshots

Screenshot 2023-10-24 at 12-04-56 Select Publishable Entity to view Studio Administration

Screenshot 2023-10-24 at 12-05-43 View Publishable Entity Studio Administration

Screenshot 2023-10-24 at 12-05-52 View Component Studio Administration

Screenshot 2023-10-24 at 12-06-05 View Component Version Studio Administration

@ormsbee ormsbee marked this pull request as ready for review October 26, 2023 21:51
Copy link
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

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

I tried it out on a small v2 library, and it worked great! Code LGTM.

ContentLibrary first.
"""
content_library = models.OneToOneField(
ContentLibrary,
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the advantage/rationale for having this as a separate model/table, rather than just adding the learning_package foreign key to ContentLibrary directly?

I ask because this code was unclear until I looked up what contents was:

            if learning_package_already_exists:
                lp = lib.contents.learning_package
                log.info(f"Deleting existing LearningPackage {lp.key} ({lp.uuid})")
                lib.contents.delete()
                lp.delete()

Copy link
Member

@kdmccormick kdmccormick Nov 27, 2023

Choose a reason for hiding this comment

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

@ormsbee I have the same question as @bradenmacdonald --why not add the FK to ContentLibrary directly?

Also, relatedly, what's the desired arity of the ContentLibrary->LearningPackage relationship? If I'm reading this model right, it's N->(0,1), but I'd imagine that the desired arity would be either 1->(0,1) or 1->1.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ContentLibrary->LearningPackage relationship was something that I went back and forth about when trying to square the idea of multiple v1 libraries being mappable into a single v2 library. But I think I agree with both of you that it should be 1:1 for now, so I'll swap this for a simple ForeignKey.

# Otherwise, it's just been modified...
else:
component_pk = published_component_pks[file_path]
version_num = 2
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a little odd to me that we have to carefully manage the version numbers like this, rather than having create_component_version return the new version numbers after success.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed.

@ormsbee I don't want to block this PR, so consider adding something like create_next_component_version to the API in order to smooth out the I-just-want-to-create-the-next-version use case, while leaving create_component_version as-is for advanced cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will, thanks. One of the reasons I kept it explicit is because I wanted to have a pattern where the caller has to explicitly say what version they think it should be in order to catch race conditions with multiple writers and force one of them to figure it out rather than to silently overwrite. But it's just clunky when used in this scenario.

Copy link
Member

Choose a reason for hiding this comment

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

I hadn't thought about race conditions. If you end up deciding to keep it explicit because of those, that sounds good to me too.

components_api.add_content_to_component_version(
component_version,
raw_content_id=text_content.pk,
key="definition.xml",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bradenmacdonald: I just copied the blockstore convention of definition.xml here, but I was wondering if I should change it to source.xml because it doesn't really represent the definition scope, and I was wondering if it might cause more confusion than it's worth.

Or some other name? @kdmccormick: any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ormsbee Good point. I like source.xml or block.xml

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking on this a little more, source.xml might get confused with Python source code (e.g. if we later allow a src dir in there for more convenient editing, as a sibling directory for static). So with that in mind, I'll go with block.xml.

Thank you!

Copy link
Member

Choose a reason for hiding this comment

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

block.xml sounds great.

Copy link
Member

@kdmccormick kdmccormick left a comment

Choose a reason for hiding this comment

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

Looking good! A few comments, but my blocking request is the OPENEDX_LEARNING docstring.

I'll test this out on my local tutor this afternoon.

]

OPENEDX_LEARNING = {
Copy link
Member

Choose a reason for hiding this comment

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

Could you slap a setting annotation on this and the LMS setting, even if it's just a big WIP warning with a link to openedx-learning?

for deleted_definition_file in deleted_definition_files:
log.info(f"Deleting {deleted_definition_file} from draft")
component_pk = published_component_pks[deleted_definition_file]
publishing_api.set_draft_version(component_pk, None)
Copy link
Member

Choose a reason for hiding this comment

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

(openedx-learning API feedback, not blocking for this PR)

I'm enjoying this PR because it's cool seeing the publishing and components API in action in edx-platform. For the most part, each call to the APIs has been very clear and self-explanatory. Kudos.

For this particular set_draft_version call, though, I had to dive into the definition to understand that passing in None means "remove from the draft". It feels like that function does one of two different things depending on whether the second arg is None. I wonder if, rather than having a single API function which takes an Optional, it'd be better to have one API function which demands and int, and another which removes the draft:

def set_draft_version(publishable_entity_id: int, publishable_entity_version_pk: int) -> None:
    ...

def remove_from_draft(publishable_entity_id: int) -> None:
    ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. I suspect I was just getting too wrapped up in the data model to step back and think about it from the API consumer's point of view. I'll do this.

Comment on lines +1816 to +1820

# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components.apps.ComponentsConfig",
"openedx_learning.core.contents.apps.ContentsConfig",
"openedx_learning.core.publishing.apps.PublishingConfig",
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
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components.apps.ComponentsConfig",
"openedx_learning.core.contents.apps.ContentsConfig",
"openedx_learning.core.publishing.apps.PublishingConfig",
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components",
"openedx_learning.core.contents",
"openedx_learning.core.publishing",

(non-blocking nit)

Django lets you abbreviate these when your app contains an apps.py with a lone/default app config.

Comment on lines +3321 to +3325

# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components.apps.ComponentsConfig",
"openedx_learning.core.contents.apps.ContentsConfig",
"openedx_learning.core.publishing.apps.PublishingConfig",
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
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components.apps.ComponentsConfig",
"openedx_learning.core.contents.apps.ContentsConfig",
"openedx_learning.core.publishing.apps.PublishingConfig",
# Learning Core Apps, used by v2 content libraries (content_libraries app)
"openedx_learning.core.components",
"openedx_learning.core.contents",
"openedx_learning.core.publishing",

(non-blocking nit) Same comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants