-
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
feat: Legacy Libraries Migration (Prototype) #35758
feat: Legacy Libraries Migration (Prototype) #35758
Conversation
22c4880
to
6fb0955
Compare
664021b
to
4252351
Compare
Sandbox deployment successful 🚀 |
4252351
to
985a912
Compare
# If so, then we know that this block was derived from block in a legacy (v1) content library. | ||
# Try to get that block's migrated (v2) content library equivalent and use it as our upstream. | ||
elif downstream.parent and downstream.parent.block_type == "library_content": | ||
from xmodule.library_content_block import LegacyLibraryContentBlock |
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.
nit: do we need this import here?
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.
For the type annotation on the next line. Importing at the top of the file would be a cyclical import.
Alternatively, I could just # type: ignore
the next line, but I'd rather avoid that.
EDIT: I think I could do this at the top of the file:
if t.TYPE_CHECKING:
from xmodule.library_content_block import LegacyLibraryContentBlock
I'll try that when this PR is ready for actual review.
I'm in favor of having an explicit table for a downstream - upstream mapping.
As long as the relation is only a mapping between a |
985a912
to
2d66c67
Compare
Sandbox deployment successful 🚀 |
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.
@kdmccormick Exciting! Thanks for the prototype here.
Question: Is the content_libraries app the right place for these? Would they be better in contentstore, since they are CMS-focused?
Where possible, I do like to keep legacy/transition code separate from new/permanent code. So I'd lean toward openedx/core/djangoapps/content_libraries/migration/*
as the home. But split_modulestore_django
or contentstore
are also fine with me.
These models can be deleted as soon as v1 libraries are fully removed, right?
Alternatively, do we want to generalize these models so that they could be used as a basis for the course migration as well?
We're so far from being able to migrate courses that I don't think we know enough to say what the data model requirements will be. For example, I think we'll have a 1:1 mapping from courses to learning package, but that's not the case for libraries. So I think it's premature to try and share the code. I'm not opposed to putting the library migration tracking model in split_modulestore_django
though.
This LegacyLibraryContentBlock's source library has been migrated. You can see that the child is now considered to be "Sourced from a library" based on the icon. If I publish an edit to this problem in its new library, instead of seeing the legacy "Update Now" messaging from the LegacyLibraryContentBlock, I'd see the new "Update Available" button on the problem itself (I would show this but my authoring env is currently broken).
Help me understand here. So, we're letting users migrate libraries one at a time. As they do, if they happen to navigate into a course where a LegacyLibraryContentBlock references that, then our "upstream detection" code will treat them the same way as children of Problem Banks (that is, they are downstream copies from an upstream v2 library). But we haven't actually modified the LegacyLibraryContentBlock nor its children yet at this stage. So what happens if I edit the settings of the LegacyLibraryContentBlock , say increasing the number of children to display. Or what if I want to choose a different (v2) library/collection or anything else. Will I have to delete the LegacyLibraryContentBlock and manually replace it with a Problem Bank?
I guess what I'm asking is if we should instead scan for relevant LegacyLibraryContentBlocks and convert them to Problem Banks as an explicit migration process instead of having to handle these various hybrid half-migrated situations as they arise.
Timing
Oh, and please don't merge this PR anytime soon, while we're still in bugfix mode for Sumac :)
|
||
# In order identify the new v2 library block, we need to know the v1 library block that this child came from. | ||
# Unfortunately, there's no straightforward mapping from these children back to their v1 library source blocks. | ||
# (ModuleStore does have a get_original_usage function that inspects edit_info, but we can't count on |
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.
# (ModuleStore does have a get_original_usage function that inspects edit_info, but we can't count on | |
# (ModuleStore does have a get_block_original_usage function that inspects edit_info, but we can't count on |
# (ModuleStore does have a get_original_usage function that inspects edit_info, but we can't count on | ||
# that always working, particularly if this block's course was imported from another instance.) | ||
# However, we can work around this by just looping through every block in the legacy library, and testing to see | ||
# if it's our source block. |
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.
Can't we at least start with get_block_original_usage
and fall back to this loop if it doesn't work?
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.
+1, but beware that at least one invocation of get_block_original_usage
has cleanup code that implies it can pass back a versioned UsageKey:
edx-platform/xmodule/library_content_block.py
Lines 408 to 411 in d197077
orig_key, orig_version = self.runtime.modulestore.get_block_original_usage(usage_key) | |
return { | |
"usage_key": str(usage_key), | |
"original_usage_key": str(orig_key.replace(version=None, branch=None)) if orig_key else 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 call, and noted
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 is really exciting stuff! 😁
""" | ||
source_key = LearningContextKeyField(unique=True, max_length=255) | ||
target = models.ForeignKey(ContentLibrary, on_delete=models.CASCADE) | ||
target_collection = models.ForeignKey(Collection, on_delete=models.SET_NULL, null=True) |
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 model encodes some very reasonable lifecycle behavior that is probably worth explicitly calling out in the comments, (e.g. "deleting the v2 library deletes any record of the migration", "collection is nullable because you can migrate and then delete the target collection, without actually deleting the components in that collection", etc.).
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.
Can do
|
||
class ContentLibraryBlockMigration(models.Model): | ||
""" | ||
Record of a legacy (v1) content library block that has been migrated into a new (v) content library block. |
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.
Record of a legacy (v1) content library block that has been migrated into a new (v) content library block. | |
Record of a legacy (v1) content library block that has been migrated into a new (v2) content library block. |
block_type = models.SlugField() | ||
source_block_id = models.SlugField() |
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 is fine, but I'm curious why you didn't decide to use a UsageKeyField
here instead?
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.
Perhaps I was over-normalizing. I was trying make it impossible to encode three invalid cases:
- The block type of the source doesn't make the block type of the target.
- The LearningContexts of the source doesn't match
library_migration.source_key
- The LearningContexts of the target doesn't match
library_migration.target
Sounds like it would be simpler to make the source a UsageKeyField and the target a Component, plus a mix of a db constraints and app-level validation to avoid those invalid 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.
Dave: Also, this is an edge case, but do we want to explicitly disallow having two separate v1 blocks map to the same v2 block?
Yes, I'll add that constraint.
if not self.source_library_id: | ||
raise NoUpstream() | ||
try: | ||
source_library_key = self.source_library_key |
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.
Nit: I think that properties that can throw exceptions like this are foot-guns waiting to happen. I realize that the source_library_key
property method precedes this PR, so take this as a purely optional item.
# (ModuleStore does have a get_original_usage function that inspects edit_info, but we can't count on | ||
# that always working, particularly if this block's course was imported from another instance.) | ||
# However, we can work around this by just looping through every block in the legacy library, and testing to see | ||
# if it's our source block. |
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.
+1, but beware that at least one invocation of get_block_original_usage
has cleanup code that implies it can pass back a versioned UsageKey:
edx-platform/xmodule/library_content_block.py
Lines 408 to 411 in d197077
orig_key, orig_version = self.runtime.modulestore.get_block_original_usage(usage_key) | |
return { | |
"usage_key": str(usage_key), | |
"original_usage_key": str(orig_key.replace(version=None, branch=None)) if orig_key else None, |
Thanks all for your feedback. To avoid spamming you, I'm going to close this PR and re-open a new one (which I will factor your feedback into). I'd like to implement this on top of the upstream-downstream link models that @navinkarkera is taking the lead on currently, so the migration blocked by: openedx/modular-learning#242 |
Part of #32457
What's ready for feedback
Firstly, the migration data models in
openedx/core/djangoapps/content_libraries/models.py
The new migration Python API function, defined in
openedx/core/djangoapps/content_libraries/migration_api.py
.Finally, the new upstream link computation for LegacyLibraryContentBlock children, which would allow them to seamlessly switch over to syncing from V2 Libraries once their source is migrated, without any backwards incompatibility, user intervention, or spooky background content changes. Defined in
cms/lib/xblock/upstream_sync.py
andxmodule/library_content_block.py
.This LegacyLibraryContentBlock's source library has been migrated. You can see that the child is now considered to be "Sourced from a library" based on the icon. If I publish an edit to this problem in its new library, instead of seeing the legacy "Update Now" messaging from the LegacyLibraryContentBlock, I'd see the new "Update Available" button on the problem itself (I would show this but my authoring env is currently broken).
What's missing
Hacky stuff in this PR, for demo purposes
This PR has an admin action on the split_django_modulestore index, and new
migrate_legacy_library
CMS management command. It could be good to clean one or both of these up and merge them into master so that developers can experiment with the migration. (This would not replace the need for a similar end-user-facing UI in Teak).This PR also modifies the titles and URL targets of the Legacy Libraries home tab. Clicking on a migrated library will send you to the new library and/or collection within that library. For Teak, you could imagine a nicer-looking version of this, showing users which legacy libraries have been migrated where. Alternatively, we could just hide migrated legacy libraries from this list.
Sandbox env
I'm still getting Meili set up on this...
Link: https://studio.pr-35758-7daceb.sandboxes.opencraft.hosting/
UN/PW: openedx / openedx
Settings
Tutor requirements