Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Table of Contents

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” }

...

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: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” }

...

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:libBlockY")} { display_name: “title Y” } { display_name: “override title Y” } { data: “yyy” }

...

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

...

Table of Contents

Legend

  • opaque_key { < settings-scoped defaults > } { < settings-scoped fields > } { < content-scoped fields > } # comment

    • child_1_opaque_key

    • etc

Scenario

We have a course with a library_content block pointing at version 5 of a library, containing blocks W, X, Y, Z.

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

The latest version of the library (6) makes some changes:

  • W and X are deleted.

  • Y and Z are kept, although Z’s content is edited.

  • Q is added.

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}

      • # libBlockW removed

      • # libBlockX removed

      • lb:O:L:problem:libBlockY { display_name: “title Y” } { data: “yyy” }

      • lb:O:L:problem:libBlockZ { display_name: “title Z” } { data: “zzz_updated” } # content changed since v5

      • lb:O:L:problem:libBlockQ { display_name: “title Q” } { data: “qqq” } # new

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 for 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 for 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 it wouldn’t really fit efficiently or elegantly with Learning Core’s data model, which optimizes for loading versions of individual components rather than content libraries.

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?

TODO… write down schema for defaults, something like:

Code Block
<problem display_name="bob" default-display_name="superbob" />

TODO… write down default vs inheritance semantics, something like:

  • Parent { setting1: parentValue }

    • Child {defaults : { setting1: value1 }} → setting1: parentValue

  • Unit { setting1: parentValue }

    • LCB

      • Child {defaults : { setting1: value1 }} → setting1: parentValue

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 seem all well and good, 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. We’d expect that the settings overrides and student state for blocks W and X get thrown away, since those blocks are removed in library version 6. But we wouldn’t expect that the settings and student state of Y and Z would also be thrown out! (Note that the content edits are also thrown out, but that’s standard when it comes to updating a library to the latest version.) The messed-up structure would look like this:

  • 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” }

            • # libBlockW was deleted from the library – it gets removed here, as expected.

            • # libBlockX was deleted from the library – it gets removed here, as expected.

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockY")}{ display_name: “title Y” } { data: “yyy” } # R.I.P. settings and student state :(

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}{ display_name: “title Z” }{ data: “zzz_updated” } # R.I.P. settings and student state :(

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockQ")}{ display_name: “title Q” }{ data: “qqq” } # Brand new

How does V1 handle this? Well, it special-cases the duplication function, telling it 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+R2R+type@sequence+block@...

        • block-v1:O+C+R2R+type@vertical+block@...

          • block-v1:O+C+R2R+type@library_content+block@myLCB { source_library_id: “lib:O:L”, source_library_version: “5” }

            • block-v1:O+C+R2R+type@problem+block@{DERIVE_ID("myLCB", "lb:O:L:problem:libBlockW")}{ display_name: “title W” } { data: “www” }

            • block-v1:O+C+R2R+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+R2R+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+R2R+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

...

          • block-v1:O+C+R+type@library_content+block@dupeLCB { source_library_id: “lib:O:L”, source_library_version: “5” }

            • # no children yet

Then, in the studio_post_duplicate callback, it reloads the blocks from the library, yielding:

  • 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@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead1block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockW")}{ display_name: “title W” } { data: “www” }

            • block-v1:O+C+R+type@problem+block@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead2block@{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@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead3+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@deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead4block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}{ 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 lostThat’s better (although content edits are lost, just like they are for V1 import 😞 ). Now, if dupeLBC’s library version were to be updated, we’d retain settings overrides and student state for the blocks that stick around between versions 5 and 6:

  • 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” }

            • # libBlockW was deleted from the library – it gets removed here, as expected.

            • # libBlockX was deleted from the library – it gets removed here, as expected.

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockWlibBlockY")}{ 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")}{ Y” } { display_name: “title X” “override title Y” } { data: “xxx” “yyy” } # R.I.P. STUDENT STATEStudent state remains!

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockYlibBlockZ")}{ display_name: “title Y” Z” }{ data: “yyy” } # R.I.P. STUDENT STATE“zzz_updated” } # Student state remains!

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZlibBlockQ")}{ display_name: “title Z” Q” }{ data: “zzz” “qqq” } # 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@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@library_content+block@dupeLCB { source_library_id: “lib:O:L”, source_library_version: “5” }

...

            • Brand new

How do we want to do this for V2 libraries? Better, that’s how. Essentially, we want to copy the blocks, thus preserving content overrides, but we’re going to explicitly set the usage keys to what their derived values should be, thus linking up the duplicated child blocks with their upstream library counterparts.

The thing is, this requires knowing what the source library blocks' keys were. In these visuals, we’ve been spelling them out (lb:O:L:problem:libBlock_), but the LibraryContentBlock doesn’t actually store them--it just stores its children, whose usage keys have been spit out from the non-reversible DERIVE_ID function.

So how do we get back the library blocks' usage keys? Well, it’s simple. We just grab the blocks in the latest version of the library, loop through them calling DERIVE_KEY, and rebuild a mapping from child keys to library keys! That was probably hard to follow. Here’s some pseudocode:

Code Block
languagepy
def compute_library_content_children_mapping(original_lcb: LibraryContentBlock, dupe_lcb: LibraryContentBlock) -> dict[UsageKey, UsageKey]:

    original_child_ids_to_library_keys: dict[str, LibraryBlockLocatorV2] = {
        DERIVE_ID(original_lcb.usage_key.block_id, source_lib_block_key): source_library_usage_key
        for source_lib_usage_key in get_library_block_usage_keys(original_lcb.source_library_id)
    }
  
    original_child_ids_to_dupe_child_ids: dict[str, str] = {
        (
            # Block exists in latest lib version. Upgrading lib should preserve it. Derive dupe child key so that it matches!
            DERIVE(dupe_lcb.usage_key.block_id, original_child_ids_to_library_keys[original_child_key.block_id])
            if original_child_key.  block_id in original_child_ids_to_library_keys

            # Block is gone in latest lib version. Upgrading lib will destroy it. So, its duplicated child key is arbitrary!
            else f"{original_child_key.block_id}_unlinked_copy"
        )
        for original_child_key in original_lcb.children
    }
        
    return {
        original_lcb.context_key.make_usage_key(original_child_id)
        dupe_lcb.context_key.make_usage_key(dupe_child_id)
        for original_child_id, dupe_child_id in original_child_ids_to_dupe_child_ids.items()
    }

Performing this operation, we get:

  • 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("dupeLCBmyLCB", "lb:O:L:problem:libBlockW")}_unlinked_copy{ display_name: “title W” } { data: “www” }

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCBmyLCB", "lb:O:L:problem:libBlockX")}_unlinked_copy{ 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” } # Edit remains!!

            • 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:

...

languagepy

...

            • data: “zzz_edit” } # Edit remains!!

And, if the user upgraded the dupe’s source version, they’d happily see:

  • 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:

...

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:

  • Code Block
    languagepy
    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)

...

            • 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” }

            • # libBlockW was deleted from the library – it gets removed here, as expected.

            • # libBlockX was deleted from the library – it gets removed here, as expected.

            • 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” } # Student state remains!

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockZ")}{ display_name: “title Z” }{ data: “zzz_updated” } # Student state remains!

            • block-v1:O+C+R+type@problem+block@{DERIVE_ID("dupeLCB", "lb:O:L:problem:libBlockQ")}{ display_name: “title Q” }{ data: “qqq” } # Brand new