Skip to content

feat!: Add Container.container_code field#545

Open
kdmccormick wants to merge 1 commit intokdmccormick/keys-componentfrom
kdmccormick/keys-container
Open

feat!: Add Container.container_code field#545
kdmccormick wants to merge 1 commit intokdmccormick/keys-componentfrom
kdmccormick/keys-container

Conversation

@kdmccormick
Copy link
Copy Markdown
Member

@kdmccormick kdmccormick commented Apr 15, 2026

Description

Adds a container_code field (code_field) and a learning_package FK to the Container model. Also adds a UniqueConstraint on (learning_package, container_code), which is stricter than Component's constraint (no type scoping -- container codes must be unique across all container types within a given LearningPackage).

For existing containers, container_code is backfilled from the entity key via a data migration. Future containers will have container_code set explicitly by the caller.

Backup/restore now writes container_code into the [entity.container] section (Verawood and later). Archives created in Ulmo (which have no container_code) fall back to using the entity key as the container_code on restore.

BREAKING CHANGE: create_container() and create_container_and_version() now require a container_code keyword argument. The same applies to create_unit_and_version(), create_subsection_and_version(), and create_section_and_version().

Part of: #322

Full series of PRs:

  1. feat!: Collection.key -> Collection.collection_code #542
  2. feat!: Component.local_key -> Component.component_code #544
  3. feat!: Add Container.container_code field #545
  4. feat!: Package and Entity keys are now opaque refs #546
  5. feat!: ComponentVersionMedia.key -> ComponentVersionMedia.path #547

Testing, AI Usage, and Merge Considerations

See #322

learning_package_id: LearningPackage.ID,
key: str,
*,
container_code: str,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The fact that this has key and container_code is just from how the PRs are split up right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yep

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, I'm not sure why we have them as separate arguments here instead of just using container_code = key and then later renaming the key arg to container_code, but it really doesn't matter if it's just for this PR and it all get sorted out in the end.

@kdmccormick kdmccormick force-pushed the kdmccormick/keys-component branch from fbe4b4b to f4d32b8 Compare April 16, 2026 18:18
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-container branch from 266d7f3 to 5283857 Compare April 16, 2026 20:47
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-component branch from f4d32b8 to 0f875ed Compare April 17, 2026 13:40
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-container branch from 5283857 to c32f78c Compare April 17, 2026 14:28
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-component branch from 0f875ed to 2bf60ff Compare April 17, 2026 16:35
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-container branch 2 times, most recently from 8434cbf to 64f0eeb Compare April 17, 2026 16:56
@kdmccormick kdmccormick marked this pull request as ready for review April 17, 2026 17:57
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-component branch from 2bf60ff to 382a7b3 Compare April 17, 2026 18:45
Adds a container_code field (code_field) and a learning_package FK to the
Container model. Also adds a UniqueConstraint on (learning_package,
container_code), which is stricter than Component's constraint
(no type scoping -- container codes must be unique across all container
types within a given LearningPackage).

For existing containers, container_code is backfilled from the entity key
via a data migration. Future containers will have container_code set
explicitly by the caller.

Backup/restore now writes container_code into the [entity.container]
section (Verawood and later). Archives created in Ulmo (which have no
container_code) fall back to using the entity key as the container_code
on restore.

BREAKING CHANGE: create_container() and create_container_and_version()
now require a container_code keyword argument. The same applies to
create_unit_and_version(), create_subsection_and_version(), and
create_section_and_version().

Part of: #322

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kdmccormick kdmccormick force-pushed the kdmccormick/keys-container branch from 64f0eeb to f67afa4 Compare April 17, 2026 18:47
# a single LearningPackage through our publishable_entity relation. However,
# having this foreign key directly allows us to make indexes that efficiently
# query by other Container fields within a given LearningPackage.
learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Very much non-blocking:

I wish we had some way to enforce the integrity of these denormalized relationships.

One idea could be in the future to write a system check that inspects the database and verifies everything. System checks which depend on the database do not normally run on startup (for performance) but can by run manually with the ./manage.py check command.

Copy link
Copy Markdown
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'm fine with this, assuming it's immediately followed by the proper removal of the key field, but I'd prefer to replace key more 1 for 1 as mentioned. Did not run the code, just read.

models.UniqueConstraint(
fields=["learning_package", "container_code"],
name="oel_container_uniq_lp_cc",
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Optional: Suggest adding code constraint here too.

Comment on lines 34 to +36
key: str,
*,
container_code: str,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
key: str,
*,
container_code: str,
container_code: str,
*,

This would make more sense to me, and then pass key=container_code, container_code=container_code or just container_code=container_code in the part below.

But if this is definitely just a temporary thing until the next PR merges, no problem.

"my_container",
created=self.now,
created_by=None,
container_code="my_container",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I mentioned this on the key removal PR, but do we really want to change this code from a positional second arg to a kwarg? Leaving it as positional second arg will make this change simpler on the platform side and even reduce the diff in this test code.

Non-blocking.

Copy link
Copy Markdown
Contributor

@ormsbee ormsbee left a comment

Choose a reason for hiding this comment

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

Just a sanity check question, but otherwise LGTM.

Comment on lines +18 to +22
Container = apps.get_model("openedx_content", "Container")
for container in Container.objects.select_related("publishable_entity__learning_package").all():
container.learning_package = container.publishable_entity.learning_package
container.container_code = container.publishable_entity.key
container.save(update_fields=["learning_package", "container_code"])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Comment, no action needed] It's really unfortunate that we can't use F() across relations. I feel like this is one of the few cases where I'd be tempted to use raw SQL.

name="container_code",
field=openedx_django_lib.fields.MultiCollationCharField(
db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"},
max_length=255,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's not necessary to explicitly add null=False here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@ormsbee oof, thank you for catching that. All of collection_code, container_code, and component_code are missing null=False on their definition and migrations. I'll fix them all.

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