diff --git a/src/openedx_content/applets/backup_restore/serializers.py b/src/openedx_content/applets/backup_restore/serializers.py index 4995e0cf..5e4f2e5c 100644 --- a/src/openedx_content/applets/backup_restore/serializers.py +++ b/src/openedx_content/applets/backup_restore/serializers.py @@ -114,35 +114,45 @@ class ComponentVersionSerializer(EntityVersionSerializer): # pylint: disable=ab class ContainerSerializer(EntitySerializer): # pylint: disable=abstract-method """ Serializer for containers. + + Extracts container_code from the [entity.container] section. + Archives created in Verawood or later include an explicit + ``container_code`` field. Archives created in Ulmo do not, so we + fall back to using the entity key as the container_code. """ + container = serializers.DictField(required=True) def validate_container(self, value): """ Custom validation logic for the container field. - Ensures that the container dict has exactly one key which is one of - "section", "subsection", or "unit" values. + Ensures that the container dict has exactly one type key ("section", + "subsection", or "unit"), optionally alongside "container_code". """ errors = [] - if not isinstance(value, dict) or len(value) != 1: - errors.append("Container must be a dict with exactly one key.") - if len(value) == 1: # Only check the key if there is exactly one - container_type = list(value.keys())[0] - if container_type not in ("section", "subsection", "unit"): - errors.append(f"Invalid container value: {container_type}") + type_keys = [k for k in value if k in ("section", "subsection", "unit")] + if len(type_keys) != 1: + errors.append( + "Container must have exactly one type key: 'section', 'subsection', or 'unit'." + ) if errors: raise serializers.ValidationError(errors) return value def validate(self, attrs): """ - Custom validation logic: - parse the container dict to extract the container type. + Custom validation logic: extract container_type and container_code. + + Archives created in Verawood or later supply an explicit + ``container_code`` field inside [entity.container]. Archives created + in Ulmo do not, so we fall back to using the entity key. """ - container = attrs["container"] - container_type = list(container.keys())[0] # It is safe to do this after validate_container + container = attrs.pop("container") + # It is safe to do this after validate_container + container_type = next(k for k in container if k in ("section", "subsection", "unit")) attrs["container_type"] = container_type - attrs.pop("container") # Remove the container field after processing + # Verawood+: container_code is explicit. Ulmo: fall back to entity key. + attrs["container_code"] = container.get("container_code") or attrs["key"] return attrs diff --git a/src/openedx_content/applets/backup_restore/toml.py b/src/openedx_content/applets/backup_restore/toml.py index 05f8b7f4..57eb6b20 100644 --- a/src/openedx_content/applets/backup_restore/toml.py +++ b/src/openedx_content/applets/backup_restore/toml.py @@ -118,11 +118,15 @@ def _get_toml_publishable_entity_table( entity_table.add("component", component_table) if hasattr(entity, "container"): + container = entity.container container_table = tomlkit.table() + # Write container_code explicitly so that restore (Verawood and later) + # does not need to parse the entity key. + container_table.add("container_code", container.container_code) container_types = ["section", "subsection", "unit"] for container_type in container_types: - if hasattr(entity.container, container_type): + if hasattr(container, container_type): container_table.add(container_type, tomlkit.table()) break # stop after the first match diff --git a/src/openedx_content/applets/containers/admin.py b/src/openedx_content/applets/containers/admin.py index df2a60ec..45784bd9 100644 --- a/src/openedx_content/applets/containers/admin.py +++ b/src/openedx_content/applets/containers/admin.py @@ -89,11 +89,13 @@ class ContainerAdmin(ReadOnlyModelAdmin): Django admin configuration for Container """ - list_display = ("key", "container_type_display", "published", "draft", "created") + list_display = ("container_code", "container_type_display", "published", "draft", "created") fields = [ "pk", "publishable_entity", "learning_package", + "container_code", + "container_type_display", "published", "draft", "created", @@ -101,8 +103,9 @@ class ContainerAdmin(ReadOnlyModelAdmin): "see_also", "most_recent_parent_entity_list", ] + # container_code is a model field; container_type_display is a method readonly_fields = fields # type: ignore[assignment] - search_fields = ["publishable_entity__uuid", "publishable_entity__key"] + search_fields = ["publishable_entity__uuid", "publishable_entity__key", "container_code"] inlines = [ContainerVersionInlineForContainer] def learning_package(self, obj: Container) -> SafeText: @@ -184,7 +187,7 @@ class ContainerVersionInlineForEntityList(admin.TabularInline): fields = [ "pk", "version_num", - "container_key", + "container_code", "title", "created", "created_by", @@ -203,8 +206,8 @@ def get_queryset(self, request): ) ) - def container_key(self, obj: ContainerVersion) -> SafeText: - return model_detail_link(obj.container, obj.container.key) + def container_code(self, obj: ContainerVersion) -> SafeText: + return model_detail_link(obj.container, obj.container.container_code) class EntityListRowInline(admin.TabularInline): diff --git a/src/openedx_content/applets/containers/api.py b/src/openedx_content/applets/containers/api.py index 9fe192ea..c1eae525 100644 --- a/src/openedx_content/applets/containers/api.py +++ b/src/openedx_content/applets/containers/api.py @@ -140,6 +140,7 @@ def create_container( created: datetime, created_by: int | None, *, + container_code: str, container_cls: type[ContainerModel], can_stand_alone: bool = True, ) -> ContainerModel: @@ -152,6 +153,8 @@ def create_container( key: The key of the container. created: The date and time the container was created. created_by: The ID of the user who created the container + container_code: A local slug identifier for the container, unique within + the learning package (regardless of container type). container_cls: The subclass of container to create (e.g. `Unit`) can_stand_alone: Set to False when created as part of containers @@ -170,7 +173,9 @@ def create_container( ) container = container_cls.objects.create( publishable_entity=publishable_entity, + learning_package_id=learning_package_id, container_type=container_cls.get_container_type(), + container_code=container_code, ) return container @@ -341,6 +346,7 @@ def create_container_and_version( learning_package_id: LearningPackage.ID, key: str, *, + container_code: str, title: str, container_cls: type[ContainerModel], entities: EntityListInput | None = None, @@ -354,6 +360,8 @@ def create_container_and_version( Args: learning_package_id: The learning package ID. key: The key. + container_code: A local slug identifier for the container, unique within + the learning package (regardless of container type). title: The title of the new container. container_cls: The subclass of container to create (e.g. Unit) entities: List of the entities that will comprise the entity list, in @@ -371,6 +379,7 @@ def create_container_and_version( key, created, created_by, + container_code=container_code, can_stand_alone=can_stand_alone, container_cls=container_cls, ) diff --git a/src/openedx_content/applets/containers/models.py b/src/openedx_content/applets/containers/models.py index ece19c79..dfe28c46 100644 --- a/src/openedx_content/applets/containers/models.py +++ b/src/openedx_content/applets/containers/models.py @@ -11,8 +11,9 @@ from django.db import models from typing_extensions import deprecated -from openedx_django_lib.fields import case_sensitive_char_field +from openedx_django_lib.fields import case_sensitive_char_field, code_field +from ..publishing.models.learning_package import LearningPackage from ..publishing.models.publishable_entity import ( PublishableEntity, PublishableEntityMixin, @@ -171,6 +172,12 @@ class Container(PublishableEntityMixin): olx_tag_name: str = "" _type_instance: ContainerType # Cache used by get_container_type() + # This foreign key is technically redundant because we're already locked to + # 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) + # The type of the container. Cannot be changed once the container is created. container_type = models.ForeignKey( ContainerType, @@ -179,6 +186,11 @@ class Container(PublishableEntityMixin): editable=False, ) + # container_code is an identifier that is local to the learning_package. + # Unlike component_code, it is unique across all container types within + # the same LearningPackage. + container_code = code_field() + @property def id(self) -> ID: return cast(Container.ID, self.publishable_entity_id) @@ -194,6 +206,14 @@ def pk(self): # override this with a deprecated marker, so it shows a warning in developer's IDEs like VS Code. return self.id + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["learning_package", "container_code"], + name="oel_container_uniq_lp_cc", + ), + ] + @classmethod def validate_entity(cls, entity: PublishableEntity) -> None: """ diff --git a/src/openedx_content/applets/sections/api.py b/src/openedx_content/applets/sections/api.py index 0aee1c7b..11dc1ffe 100644 --- a/src/openedx_content/applets/sections/api.py +++ b/src/openedx_content/applets/sections/api.py @@ -33,6 +33,7 @@ def create_section_and_version( learning_package_id: LearningPackage.ID, key: str, *, + container_code: str, title: str, subsections: Iterable[Subsection | SubsectionVersion] | None = None, created: datetime, @@ -49,6 +50,7 @@ def create_section_and_version( section, sv = containers_api.create_container_and_version( learning_package_id, key=key, + container_code=container_code, title=title, entities=subsections, created=created, diff --git a/src/openedx_content/applets/subsections/api.py b/src/openedx_content/applets/subsections/api.py index 5d2a6cdb..d7c7d0a5 100644 --- a/src/openedx_content/applets/subsections/api.py +++ b/src/openedx_content/applets/subsections/api.py @@ -33,6 +33,7 @@ def create_subsection_and_version( learning_package_id: LearningPackage.ID, key: str, *, + container_code: str, title: str, units: Iterable[Unit | UnitVersion] | None = None, created: datetime, @@ -49,6 +50,7 @@ def create_subsection_and_version( subsection, sv = containers_api.create_container_and_version( learning_package_id, key=key, + container_code=container_code, title=title, entities=units, created=created, diff --git a/src/openedx_content/applets/units/api.py b/src/openedx_content/applets/units/api.py index cde86bf0..dc50a86d 100644 --- a/src/openedx_content/applets/units/api.py +++ b/src/openedx_content/applets/units/api.py @@ -33,6 +33,7 @@ def create_unit_and_version( learning_package_id: LearningPackage.ID, key: str, *, + container_code: str, title: str, components: Iterable[Component | ComponentVersion] | None = None, created: datetime, @@ -49,6 +50,7 @@ def create_unit_and_version( unit, uv = containers_api.create_container_and_version( learning_package_id, key=key, + container_code=container_code, title=title, entities=components, created=created, diff --git a/src/openedx_content/migrations/0010_add_container_code.py b/src/openedx_content/migrations/0010_add_container_code.py new file mode 100644 index 00000000..acb42ddd --- /dev/null +++ b/src/openedx_content/migrations/0010_add_container_code.py @@ -0,0 +1,88 @@ +import re + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import openedx_django_lib.fields + + +def backfill_container_code(apps, schema_editor): + """ + Backfill container_code and learning_package from publishable_entity. + + For existing containers, container_code is set to the entity key (the + only identifier available at this point). Future containers will have + container_code set by the caller. + """ + 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"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("openedx_content", "0009_rename_component_local_key_to_component_code"), + ] + + operations = [ + # 1. Add learning_package FK (nullable initially for backfill) + migrations.AddField( + model_name="container", + name="learning_package", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="openedx_content.learningpackage", + ), + ), + # 2. Add container_code (nullable initially for backfill) + migrations.AddField( + model_name="container", + name="container_code", + field=openedx_django_lib.fields.MultiCollationCharField( + db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"}, + max_length=255, + null=True, + ), + ), + # 3. Backfill both fields from publishable_entity + migrations.RunPython(backfill_container_code, migrations.RunPython.noop), + # 4. Make both fields non-nullable and add regex validation to container_code + migrations.AlterField( + model_name="container", + name="learning_package", + field=models.ForeignKey( + null=False, + on_delete=django.db.models.deletion.CASCADE, + to="openedx_content.learningpackage", + ), + ), + migrations.AlterField( + model_name="container", + name="container_code", + field=openedx_django_lib.fields.MultiCollationCharField( + db_collations={"mysql": "utf8mb4_bin", "sqlite": "BINARY"}, + max_length=255, + validators=[ + django.core.validators.RegexValidator( + re.compile(r"^[a-zA-Z0-9_.-]+\Z"), + "Enter a valid \"code name\" consisting of letters, numbers, " + "underscores, hyphens, or periods.", + "invalid", + ), + ], + ), + ), + # 5. Add uniqueness constraint + migrations.AddConstraint( + model_name="container", + constraint=models.UniqueConstraint( + fields=["learning_package", "container_code"], + name="oel_container_uniq_lp_cc", + ), + ), + ] diff --git a/tests/openedx_content/applets/backup_restore/test_backup.py b/tests/openedx_content/applets/backup_restore/test_backup.py index 6e9dc733..1ece3188 100644 --- a/tests/openedx_content/applets/backup_restore/test_backup.py +++ b/tests/openedx_content/applets/backup_restore/test_backup.py @@ -163,6 +163,7 @@ def setUpTestData(cls): key="unit-1", created=cls.now, created_by=cls.user.id, + container_code="unit-1", container_cls=Unit, ) diff --git a/tests/openedx_content/applets/backup_restore/test_restore.py b/tests/openedx_content/applets/backup_restore/test_restore.py index f1cfee70..6a8b16d2 100644 --- a/tests/openedx_content/applets/backup_restore/test_restore.py +++ b/tests/openedx_content/applets/backup_restore/test_restore.py @@ -8,7 +8,7 @@ from django.core.management import call_command from django.test import TestCase -from openedx_content.applets.backup_restore.serializers import ComponentSerializer +from openedx_content.applets.backup_restore.serializers import ComponentSerializer, ContainerSerializer from openedx_content.applets.backup_restore.zipper import LearningPackageUnzipper, generate_staged_lp_key from openedx_content.applets.collections import api as collections_api from openedx_content.applets.components import api as components_api @@ -429,6 +429,44 @@ def test_invalid_component_type_format(self): assert "component" in s.errors +class ContainerSerializerTest(TestCase): + """ + Unit tests for ContainerSerializer's back-compat handling of Ulmo archives + (no container_code) vs. Verawood+ archives (explicit container_code). + """ + + BASE_DATA = { + "can_stand_alone": True, + "key": "unit1-b7eafb", + "created": "2025-09-04T22:51:59Z", + } + + def _serialize(self, container_dict): + data = {**self.BASE_DATA, "container": container_dict} + s = ContainerSerializer(data=data) + s.is_valid() + return s + + def test_verawood_explicit_container_code(self): + """Verawood+ archives include container_code in [entity.container].""" + s = self._serialize({"unit": {}, "container_code": "my_unit"}) + assert s.is_valid(), s.errors + assert s.validated_data["container_type"] == "unit" + assert s.validated_data["container_code"] == "my_unit" + + def test_ulmo_fallback_to_entity_key(self): + """Ulmo archives have no container_code; fall back to entity key.""" + s = self._serialize({"unit": {}}) + assert s.is_valid(), s.errors + assert s.validated_data["container_type"] == "unit" + assert s.validated_data["container_code"] == "unit1-b7eafb" + + def test_invalid_no_type_key(self): + """Container dict must have exactly one type key.""" + s = self._serialize({"container_code": "my_unit"}) + assert not s.is_valid() + + class RestoreUtilitiesTest(TestCase): """Tests for utility functions used in the restore process.""" diff --git a/tests/openedx_content/applets/collections/test_api.py b/tests/openedx_content/applets/collections/test_api.py index daf839cf..17240ab3 100644 --- a/tests/openedx_content/applets/collections/test_api.py +++ b/tests/openedx_content/applets/collections/test_api.py @@ -272,6 +272,7 @@ def setUpTestData(cls) -> None: key="unit-1", created=created_time, created_by=cls.user.id, + container_code="unit-1", container_cls=Unit, ) diff --git a/tests/openedx_content/applets/containers/test_api.py b/tests/openedx_content/applets/containers/test_api.py index f40f16cd..3125a771 100644 --- a/tests/openedx_content/applets/containers/test_api.py +++ b/tests/openedx_content/applets/containers/test_api.py @@ -141,13 +141,17 @@ def _other_lp_child(lp2: LearningPackage) -> TestEntity: def create_test_container( - learning_package: LearningPackage, key: str, entities: containers_api.EntityListInput, title: str = "" + learning_package: LearningPackage, + container_code: str, + entities: containers_api.EntityListInput, + title: str = "", ) -> TestContainer: """Create a TestContainer with a draft version""" container, _version = containers_api.create_container_and_version( learning_package.id, - key=key, - title=title or f"Container ({key})", + key=container_code, + container_code=container_code, + title=title or f"Container ({container_code})", entities=entities, container_cls=TestContainer, created=now, @@ -161,7 +165,7 @@ def _parent_of_two(lp: LearningPackage, child_entity1: TestEntity, child_entity2 """An TestContainer with two children""" return create_test_container( lp, - key="parent_of_two", + container_code="parent_of_two", title="Generic Container with Two Unpinned Children", entities=[child_entity1, child_entity2], ) @@ -177,7 +181,7 @@ def _parent_of_three( """An TestContainer with three children, two of which are pinned""" return create_test_container( lp, - key="parent_of_three", + container_code="parent_of_three", title="Generic Container with Two 📌 Pinned Children and One Unpinned", entities=[child_entity3.versioning.draft, child_entity2.versioning.draft, child_entity1], ) @@ -193,7 +197,7 @@ def _parent_of_six( """An TestContainer with six children, two of each entity, with different pinned combinations""" return create_test_container( lp, - key="parent_of_six", + container_code="parent_of_six", title="Generic Container with Two 📌 Pinned Children and One Unpinned", entities=[ # 1: both unpinned, 2: both pinned, and 3: pinned and unpinned @@ -217,6 +221,7 @@ def _grandparent( grandparent, _version = containers_api.create_container_and_version( lp.id, key="grandparent", + container_code="grandparent", title="Generic Container with Two Unpinned TestContainer children", entities=[parent_of_two, parent_of_three], container_cls=ContainerContainer, @@ -236,6 +241,7 @@ def _container_of_uninstalled_type(lp: LearningPackage, child_entity1: TestEntit container, _ = containers_api.create_container_and_version( lp.id, key="abandoned-container", + container_code="abandoned-container", title="Abandoned Container 1", entities=[child_entity1], container_cls=TestContainer, @@ -253,6 +259,7 @@ def _other_lp_parent(lp2: LearningPackage, other_lp_child: TestEntity) -> TestCo other_lp_parent, _version = containers_api.create_container_and_version( lp2.id, key="other_lp_parent", + container_code="other_lp_parent", title="Generic Container with One Unpinned Child Entity", entities=[other_lp_child], container_cls=TestContainer, @@ -303,6 +310,7 @@ def test_create_generic_empty_container(lp: LearningPackage, admin_user) -> None container, container_v1 = containers_api.create_container_and_version( lp.id, key="new-container-1", + container_code="new-container-1", title="Test Container 1", container_cls=TestContainer, created=now, @@ -356,10 +364,14 @@ def test_create_container_queries(lp: LearningPackage, child_entity1: TestEntity } # The exact numbers here aren't too important - this is just to alert us if anything significant changes. with django_assert_num_queries(31): - containers_api.create_container_and_version(lp.id, key="c1", **base_args) + containers_api.create_container_and_version( + lp.id, key="c1", container_code="c1", **base_args + ) # And try with a a container that has children: with django_assert_num_queries(32): - containers_api.create_container_and_version(lp.id, key="c2", **base_args, entities=[child_entity1]) + containers_api.create_container_and_version( + lp.id, key="c2", container_code="c2", **base_args, entities=[child_entity1] + ) # versioning helpers @@ -1155,6 +1167,7 @@ def test_publishing_shared_component(lp: LearningPackage): entities=[c1, c2, c3], title="Unit 1", key="unit:1", + container_code="unit-1", created=now, created_by=None, container_cls=TestContainer, @@ -1164,6 +1177,7 @@ def test_publishing_shared_component(lp: LearningPackage): entities=[c2, c4, c5], title="Unit 2", key="unit:2", + container_code="unit-2", created=now, created_by=None, container_cls=TestContainer, @@ -1276,7 +1290,7 @@ def test_deep_publish_log( # Create a "great grandparent" container that contains "grandparent" great_grandparent = create_test_container( lp, - key="great_grandparent", + container_code="great_grandparent", title="Great-grandparent container", entities=[grandparent], ) @@ -1401,7 +1415,7 @@ def test_snapshots_of_published_unit(lp: LearningPackage, child_entity1: TestEnt child_entity1_v1 = child_entity1.versioning.draft # At first the container has one child (unpinned): - container = create_test_container(lp, key="c", entities=[child_entity1]) + container = create_test_container(lp, container_code="c", entities=[child_entity1]) modify_entity(child_entity1, title="Component 1 as of checkpoint 1") _, before_publish = containers_api.get_entities_in_container_as_of(container, 0) assert not before_publish # Empty list diff --git a/tests/openedx_content/applets/publishing/test_api.py b/tests/openedx_content/applets/publishing/test_api.py index b39a74ea..37860d96 100644 --- a/tests/openedx_content/applets/publishing/test_api.py +++ b/tests/openedx_content/applets/publishing/test_api.py @@ -1129,6 +1129,7 @@ def test_parent_child_side_effects(self) -> None: "my_container", created=self.now, created_by=None, + container_code="my_container", container_cls=TestContainer, ) container_v1 = containers_api.create_container_version( @@ -1209,6 +1210,7 @@ def test_bulk_parent_child_side_effects(self) -> None: "my_container", created=self.now, created_by=None, + container_code="my_container", container_cls=TestContainer, ) container_v1 = containers_api.create_container_version( @@ -1285,6 +1287,7 @@ def test_draft_dependency_multiple_parents(self) -> None: "unit_1", created=self.now, created_by=None, + container_code="unit_1", container_cls=TestContainer, ) unit_2 = containers_api.create_container( @@ -1292,6 +1295,7 @@ def test_draft_dependency_multiple_parents(self) -> None: "unit_2", created=self.now, created_by=None, + container_code="unit_2", container_cls=TestContainer, ) for unit in [unit_1, unit_2]: @@ -1347,6 +1351,7 @@ def test_multiple_layers_of_containers(self) -> None: "unit_1", created=self.now, created_by=None, + container_code="unit_1", container_cls=TestContainer, ) containers_api.create_container_version( @@ -1362,6 +1367,7 @@ def test_multiple_layers_of_containers(self) -> None: "subsection_1", created=self.now, created_by=None, + container_code="subsection_1", container_cls=TestContainer, ) containers_api.create_container_version( @@ -1446,6 +1452,7 @@ def test_publish_all_layers(self) -> None: "unit_1", created=self.now, created_by=None, + container_code="unit_1", container_cls=TestContainer, ) containers_api.create_container_version( @@ -1461,6 +1468,7 @@ def test_publish_all_layers(self) -> None: "subsection_1", created=self.now, created_by=None, + container_code="subsection_1", container_cls=TestContainer, ) containers_api.create_container_version( @@ -1495,6 +1503,7 @@ def test_direct_field_publishing_container_marks_dependencies_indirect(self) -> ) unit = containers_api.create_container( self.learning_package.id, "direct_unit", + container_code="direct_unit", created=self.now, created_by=None, container_cls=TestContainer, ) containers_api.create_container_version( @@ -1528,6 +1537,7 @@ def test_direct_field_unit_no_version_change_still_direct_true(self) -> None: ) unit = containers_api.create_container( self.learning_package.id, "no_change_unit", + container_code="no_change_unit", created=self.now, created_by=None, container_cls=TestContainer, ) unit_v1 = containers_api.create_container_version( @@ -1583,6 +1593,7 @@ def test_direct_field_publishing_component_marks_parent_indirect(self) -> None: ) unit = containers_api.create_container( self.learning_package.id, "leaf_unit", + container_code="leaf_unit", created=self.now, created_by=None, container_cls=TestContainer, ) containers_api.create_container_version( @@ -1619,6 +1630,7 @@ def test_direct_field_both_selected_both_direct(self) -> None: ) unit = containers_api.create_container( self.learning_package.id, "both_unit", + container_code="both_unit", created=self.now, created_by=None, container_cls=TestContainer, ) containers_api.create_container_version( @@ -1645,6 +1657,7 @@ def test_container_next_version(self) -> None: "my_container", created=self.now, created_by=None, + container_code="my_container", container_cls=TestContainer, ) assert container.versioning.latest is None diff --git a/tests/openedx_content/applets/sections/test_api.py b/tests/openedx_content/applets/sections/test_api.py index f4807640..3ccbf290 100644 --- a/tests/openedx_content/applets/sections/test_api.py +++ b/tests/openedx_content/applets/sections/test_api.py @@ -36,24 +36,28 @@ def setUp(self) -> None: } self.unit_1, self.unit_1_v1 = content_api.create_unit_and_version( key="unit_1", + container_code="unit_1", title="Grandchild Unit 1", components=[self.component_1, self.component_2], **common_args, ) self.unit_2, self.unit_2_v1 = content_api.create_unit_and_version( key="unit_2", + container_code="unit_2", title="Grandchild Unit 2", components=[self.component_2, self.component_1], # Backwards order from Unit 1 **common_args, ) self.subsection_1, self.subsection_1_v1 = content_api.create_subsection_and_version( key="subsection_1", + container_code="subsection_1", title="Child Subsection 1", units=[self.unit_1, self.unit_2], **common_args, ) self.subsection_2, self.subsection_2_v1 = content_api.create_subsection_and_version( key="subsection_2", + container_code="subsection_2", title="Child Subsection 2", units=[self.unit_2, self.unit_1], # Backwards order from subsection 1 **common_args, @@ -70,6 +74,7 @@ def create_section_with_subsections( section, _section_v1 = content_api.create_section_and_version( learning_package_id=self.learning_package.id, key=key, + container_code=key, title=title, subsections=subsections, created=self.now, @@ -89,6 +94,7 @@ def test_create_empty_section_and_version(self) -> None: section, section_version = content_api.create_section_and_version( learning_package_id=self.learning_package.id, key="section:key", + container_code="section-key", title="Section", created=self.now, created_by=None, diff --git a/tests/openedx_content/applets/subsections/test_api.py b/tests/openedx_content/applets/subsections/test_api.py index 74c7685b..5e49fe27 100644 --- a/tests/openedx_content/applets/subsections/test_api.py +++ b/tests/openedx_content/applets/subsections/test_api.py @@ -31,6 +31,7 @@ def setUp(self) -> None: self.unit_1, self.unit_1_v1 = content_api.create_unit_and_version( learning_package_id=self.learning_package.id, key="unit1", + container_code="unit1", title="Unit 1", components=[self.component_1, self.component_2], created=self.now, @@ -48,6 +49,7 @@ def create_subsection_with_units( subsection, _subsection_v1 = content_api.create_subsection_and_version( learning_package_id=self.learning_package.id, key=key, + container_code=key, title=title, units=units, created=self.now, @@ -67,6 +69,7 @@ def test_create_empty_subsection_and_version(self): subsection, subsection_version = content_api.create_subsection_and_version( learning_package_id=self.learning_package.id, key="subsection:key", + container_code="subsection-key", title="Subsection", created=self.now, created_by=None, diff --git a/tests/openedx_content/applets/units/test_api.py b/tests/openedx_content/applets/units/test_api.py index 364970a8..2cf892ec 100644 --- a/tests/openedx_content/applets/units/test_api.py +++ b/tests/openedx_content/applets/units/test_api.py @@ -41,6 +41,7 @@ def create_unit_with_components( unit, _unit_v1 = content_api.create_unit_and_version( learning_package_id=self.learning_package.id, key=key, + container_code=key, title=title, components=components, created=self.now, @@ -60,6 +61,7 @@ def test_create_empty_unit_and_version(self): unit, unit_version = content_api.create_unit_and_version( learning_package_id=self.learning_package.id, key="unit:key", + container_code="unit-key", title="Unit", created=self.now, created_by=None, @@ -123,6 +125,7 @@ def test_get_unit_other_container_type(self) -> None: key="test", created=self.now, created_by=None, + container_code="test", container_cls=TestContainer, ) with pytest.raises(Unit.DoesNotExist):