-
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
Import v2 libraries content into openedx-learning data models #33577
Conversation
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 tried it out on a small v2 library, and it worked great! Code LGTM.
ContentLibrary first. | ||
""" | ||
content_library = models.OneToOneField( | ||
ContentLibrary, |
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.
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()
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.
@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.
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.
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 |
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 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.
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.
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.
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 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.
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 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", |
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: 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?
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.
@ormsbee Good point. I like source.xml
or block.xml
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.
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!
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.
block.xml
sounds great.
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.
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 = { |
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.
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) |
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.
(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:
...
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.
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.
|
||
# 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", |
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.
# 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.
|
||
# 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", |
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.
# 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
Description
Initial code to move v2 libraries content into openedx-learning data models, via a new
migrate_lib_to_learning_core
management command in thecontent_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 athttp://studio.local.overhang.io:8001/admin/oel_publishing/publishableentity/
Screenshots