Legend
opaque_key
{ < settings-scoped field defaults > } { < settings-scoped fields > } { < content-scoped fields > }child_1_opaque_key
etc
Scenario
We have a course with a library_content block pointing at version 5 of the library, where version 6 is the latest
The course overrides the titles (display_name) for X and Y, but uses the default titles of W and Z.
It edits the capa content (data) for Y and Z, but uses the upstream content of W and X.
Here’s what the lib looks like:
lib:O:L
Version 5
lb:O:L:problem:libBlockW
{ display_name: “title W” } { data: “www” }lb:O:L:problem:libBlockX
{ display_name: “title X” } { data: “xxx” }lb:O:L:problem:libBlockY
{ display_name: “title Y” } { data: “yyy” }lb:O:L:problem:libBlockZ
{ display_name: “title Z” } { data: “zzz” }
Version 6 {latest}
(contents not important)
And here’s what the course tree looks like:
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
What does the user see for the blocks under myLCB? Unsurprisingly:
title W
www
override title X
xxx
override title Y
yyy_edit
title Z
zzz_edit
Curveball 1: Import/Export
We export course-v1:O+C+R
and then import it into a new course run, course-v1-O+C+R2
. Unfortunately, the library’s default settings don’t go into in the export!
Here’s what R2 looks like:
course-v1:O+C+R2
block-v1:O+C+R2+type@chapter+block@...
block-v1:O+C+R2+type@sequence+block@...
block-v1:O+C+R2+type@vertical+block@...
block-v1:O+C+R2+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ data: “www” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “override title X” } { data: “xxx” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ data: “zzz_edit” }
What does the user see now? Well, wherever a title override isn’t set, they see the the ProblemBlock’s provided default title:
Problem
www
override title X
xxx
override title Y
yyy_edit
Problem
zzz_edit
How do we work around this in V1 libs?
We re-load the blocks from the library at the proper version (5). This gives us our defaults back, but it does blow away any content edits :(
R2 would then look like this:
course-v1:O+C+R2
block-v1:O+C+R2+type@chapter+block@...
block-v1:O+C+R2+type@sequence+block@...
block-v1:O+C+R2+type@vertical+block@...
block-v1:O+C+R2+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz” }
And the user would see the right titles, but no content edits:
title W
www
override title X
xxx
override title Y
yyy
title Z
zzz
How do we want to work around this in V2 libs?
Well, first of all the current V2 content_libraries API does not support loading blocks for old library versions. We could add support for that, but we’re not sure it’s the right approach.
Instead, we’d rather add defaults to the OLX export, so that the export comes back with defaults intact, and removes the need to re-load library blocks, thus preserving library edits 🎉 The structure is now:
course-v1:O+C+R2
block-v1:O+C+R2+type@chapter+block@...
block-v1:O+C+R2+type@sequence+block@...
block-v1:O+C+R2+type@vertical+block@...
block-v1:O+C+R2+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R2+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
Yielding:
title W
www
override title X
xxx
override title Y
yyy_edit
title Z
zzz_edit
OPEN QUESTION… how will this work for courses which were exported before the v1/v2 transition?
Curveball 2: Duplication
Back to the original course, let’s say the user duplicates LCB. The standard handling of “duplicate a block” would randomly generating id strings for its children:
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
block-v1:O+C+R+type@library_content+block@dupeLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead1
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead2
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead3
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy }block-v1:O+C+R+type@problem+block@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead4
{ display_name: “title Z” } { data: “zzz” }
That would be fine, until the course author updates the library’s source version, at which point the children with randomly-generated IDs would be thrown away in favor of new children with library-block-id-derived IDs. Title overrides and any student state would be lost:
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
block-v1:O+C+R+type@library_content+block@dupeLCB
{ source_library_id: “lib:O:L”, source_library_version: “6” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” } # R.I.P. STUDENT STATEblock-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { data: “xxx” } # R.I.P. STUDENT STATEblock-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { data: “yyy” } # R.I.P. STUDENT STATEblock-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz” } # R.I.P. STUDENT STATE
How does V1 handle this? Well, it tells the duplication code to not duplicate the library_content block’s children:
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
block-v1:O+C+R+type@library_content+block@dupeLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }(no children yet)
Then, it reloads the blocks from the library. This avoids the student-state-loss risk described above, but it does, annoyingly, blow away any content edits. This behavior is essentially the same as what happens when you import a V1 library content block.
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
block-v1:O+C+R+type@library_content+block@dupeLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz” }
For V2, it’d be really nice if we could duplicate settings and content and correctly derive the content IDs:
course-v1:O+C+R
block-v1:O+C+R+type@chapter+block@...
block-v1:O+C+R+type@sequence+block@...
block-v1:O+C+R+type@vertical+block@...
block-v1:O+C+R+type@library_content+block@myLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
block-v1:O+C+R+type@library_content+block@dupeLCB
{ source_library_id: “lib:O:L”, source_library_version: “5” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockW")}
{ display_name: “title W” } { data: “www” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockX")}
{ display_name: “title X” } { display_name: “override title X” } { data: “xxx” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockY")}
{ display_name: “title Y” } { display_name: “override title Y” } { data: “yyy_edit” }block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}
{ display_name: “title Z” } { data: “zzz_edit” }
OPEN QUESTION… how do we do this? Well, if we knew the right derived keys, we could set them ourselves! But that requires knowing what the original source library block usage keys were.
Two different ideas for figuring out the old usage keys
Add a field to LibraryContentBlock that maps the DERIVE_ID(…) output back to the original library id. Essentially:
class LibraryContentBlock(XBlock): ... # Values are: { # DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockW"): "lb:O:L:problem:libBlockW", # DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockX"): "lb:O:L:problem:libBlockW", # DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockY"): "lb:O:L:problem:libBlockW", # DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ"): "lb:O:L:problem:libBlockW", # } child_usage_keys_to_library_usage_keys = Dict(...) ...
We would need to gracefully handle old blocks which don’t have this field set.
Upon duplication of a library_content block, load up the blocks from the old version of the library, run DERIVE_KEY on each of those, and compare the results to the library content block’s children:
child_usage_keys_to_library_usage_keys: dict[UsageKey, UsageKey] = {} for source_lib_block_key in get_library_block_usage_keys(library=lcb.source_library_id, version=lcb.source_library_version): child_usage_keys_to_library_usage_keys = DERIVE_KEY(lcb.usage_key.block_id, source_lib_block_key)
TODO