Skip to content

Commit 02908db

Browse files
feat: add ContainerTypes to openedx-core (v0.38.0) (#495)
- Add a ContainerType concept and corresponding database table (ContainerTypeRecord). Now every container must have a corresponding type; plain Container instances cannot be saved on their own. - The test cases for Containers are now in the publishing app and don't depend on Unit, Subsection, etc. This consolidates the core container logic, and should also make it easier to extract to a separate containers app. - The Unit, Subsection, and Section classes, apps, and test cases are substantially simplified. Because the container APIs are now type-aware you can use the generic APIs even with specific subclasses like Unit. - The `create_next_container_version` API now accepts a `Container` object. Previously it only accepted a container PK, but passing the object is more convenient * feat: add new ContainerType model, refactor Containers implementation * feat: move containers into a new applet * feat: store `olx_tag_name` in openedx_content's core container type models * perf: slightly reduce query count when computing publishing side effects * feat: improved container admin views * fix: prevent creation of plain Containers
1 parent e2ea4b1 commit 02908db

46 files changed

Lines changed: 4454 additions & 5437 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.importlinter

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ layers=
4848
# Problems, Videos, and blocks of HTML text. This is also the type we would
4949
# associate with a single "leaf" XBlock–one that is not a container type and
5050
# has no child elements.
51-
openedx_content.applets.components
51+
# The "containers" app is built on top of publishing, and is a peer to
52+
# "components" but they do not depend on each other.
53+
openedx_content.applets.components | openedx_content.applets.containers
5254

5355
# The "media" applet stores the simplest pieces of binary and text data,
5456
# without versioning information. These belong to a single Learning Package.

src/openedx_content/admin.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from .applets.backup_restore.admin import *
77
from .applets.collections.admin import *
88
from .applets.components.admin import *
9+
from .applets.containers.admin import *
910
from .applets.media.admin import *
1011
from .applets.publishing.admin import *
11-
from .applets.sections.admin import *
12-
from .applets.subsections.admin import *
13-
from .applets.units.admin import *

src/openedx_content/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .applets.backup_restore.api import *
1414
from .applets.collections.api import *
1515
from .applets.components.api import *
16+
from .applets.containers.api import *
1617
from .applets.media.api import *
1718
from .applets.publishing.api import *
1819
from .applets.sections.api import *

src/openedx_content/applets/backup_restore/toml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
1010

1111
from ..collections.models import Collection
12-
from ..publishing import api as publishing_api
12+
from ..containers import api as containers_api
1313
from ..publishing.models import PublishableEntity, PublishableEntityVersion
1414
from ..publishing.models.learning_package import LearningPackage
1515

@@ -191,7 +191,7 @@ def toml_publishable_entity_version(version: PublishableEntityVersion) -> tomlki
191191
if hasattr(version, 'containerversion'):
192192
# If the version has a container version, add its children
193193
container_table = tomlkit.table()
194-
children = publishing_api.get_container_children_entities_keys(version.containerversion)
194+
children = containers_api.get_container_children_entities_keys(version.containerversion)
195195
container_table.add("children", children)
196196
version_table.add("container", container_table)
197197
return version_table

src/openedx_content/applets/backup_restore/zipper.py

Lines changed: 76 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@
3131

3232
from ..collections import api as collections_api
3333
from ..components import api as components_api
34+
from ..containers import api as containers_api
3435
from ..media import api as media_api
3536
from ..publishing import api as publishing_api
36-
from ..sections import api as sections_api
37-
from ..subsections import api as subsections_api
38-
from ..units import api as units_api
37+
from ..sections.models import Section
38+
from ..subsections.models import Subsection
39+
from ..units.models import Unit
3940
from .serializers import (
4041
CollectionSerializer,
4142
ComponentSerializer,
@@ -804,70 +805,70 @@ def _save_components(self, learning_package, components, component_static_files)
804805
**valid_published
805806
)
806807

807-
def _save_units(self, learning_package, containers):
808-
"""Save units and published unit versions."""
809-
for valid_unit in containers.get("unit", []):
810-
entity_key = valid_unit.get("key")
811-
unit = units_api.create_unit(learning_package.id, created_by=self.user_id, **valid_unit)
812-
self.units_map_by_key[entity_key] = unit
808+
def _save_container(
809+
self,
810+
learning_package,
811+
containers,
812+
*,
813+
container_cls: containers_api.ContainerSubclass,
814+
container_map: dict,
815+
children_map: dict,
816+
):
817+
"""Internal logic for _save_units, _save_subsections, and _save_sections"""
818+
type_code = container_cls.type_code # e.g. "unit"
819+
for data in containers.get(type_code, []):
820+
entity_key = data.get("key")
821+
container = containers_api.create_container(
822+
learning_package.id,
823+
**data, # should this be allowed to override any of the following fields?
824+
created_by=self.user_id,
825+
container_cls=container_cls,
826+
)
827+
container_map[entity_key] = container # e.g. `self.units_map_by_key[entity_key] = unit`
813828

814-
for valid_published in containers.get("unit_published", []):
829+
for valid_published in containers.get(f"{type_code}_published", []):
815830
entity_key = valid_published.pop("entity_key")
816-
children = self._resolve_children(valid_published, self.components_map_by_key)
831+
children = self._resolve_children(valid_published, children_map)
817832
self.all_published_entities_versions.add(
818833
(entity_key, valid_published.get('version_num'))
819834
) # Track published version
820-
units_api.create_next_unit_version(
821-
self.units_map_by_key[entity_key],
835+
containers_api.create_next_container_version(
836+
container_map[entity_key],
837+
**valid_published, # should this be allowed to override any of the following fields?
822838
force_version_num=valid_published.pop("version_num", None),
823-
components=children,
839+
entities=children,
824840
created_by=self.user_id,
825-
**valid_published
826841
)
827842

843+
def _save_units(self, learning_package, containers):
844+
"""Save units and published unit versions."""
845+
self._save_container(
846+
learning_package,
847+
containers,
848+
container_cls=Unit,
849+
container_map=self.units_map_by_key,
850+
children_map=self.components_map_by_key,
851+
)
852+
828853
def _save_subsections(self, learning_package, containers):
829854
"""Save subsections and published subsection versions."""
830-
for valid_subsection in containers.get("subsection", []):
831-
entity_key = valid_subsection.get("key")
832-
subsection = subsections_api.create_subsection(
833-
learning_package.id, created_by=self.user_id, **valid_subsection
834-
)
835-
self.subsections_map_by_key[entity_key] = subsection
836-
837-
for valid_published in containers.get("subsection_published", []):
838-
entity_key = valid_published.pop("entity_key")
839-
children = self._resolve_children(valid_published, self.units_map_by_key)
840-
self.all_published_entities_versions.add(
841-
(entity_key, valid_published.get('version_num'))
842-
) # Track published version
843-
subsections_api.create_next_subsection_version(
844-
self.subsections_map_by_key[entity_key],
845-
units=children,
846-
force_version_num=valid_published.pop("version_num", None),
847-
created_by=self.user_id,
848-
**valid_published
849-
)
855+
self._save_container(
856+
learning_package,
857+
containers,
858+
container_cls=Subsection,
859+
container_map=self.subsections_map_by_key,
860+
children_map=self.units_map_by_key,
861+
)
850862

851863
def _save_sections(self, learning_package, containers):
852864
"""Save sections and published section versions."""
853-
for valid_section in containers.get("section", []):
854-
entity_key = valid_section.get("key")
855-
section = sections_api.create_section(learning_package.id, created_by=self.user_id, **valid_section)
856-
self.sections_map_by_key[entity_key] = section
857-
858-
for valid_published in containers.get("section_published", []):
859-
entity_key = valid_published.pop("entity_key")
860-
children = self._resolve_children(valid_published, self.subsections_map_by_key)
861-
self.all_published_entities_versions.add(
862-
(entity_key, valid_published.get('version_num'))
863-
) # Track published version
864-
sections_api.create_next_section_version(
865-
self.sections_map_by_key[entity_key],
866-
subsections=children,
867-
force_version_num=valid_published.pop("version_num", None),
868-
created_by=self.user_id,
869-
**valid_published
870-
)
865+
self._save_container(
866+
learning_package,
867+
containers,
868+
container_cls=Section,
869+
container_map=self.sections_map_by_key,
870+
children_map=self.subsections_map_by_key,
871+
)
871872

872873
def _save_draft_versions(self, components, containers, component_static_files):
873874
"""Save draft versions for all entity types."""
@@ -888,47 +889,29 @@ def _save_draft_versions(self, components, containers, component_static_files):
888889
**valid_draft
889890
)
890891

891-
for valid_draft in containers.get("unit_drafts", []):
892-
entity_key = valid_draft.pop("entity_key")
893-
version_num = valid_draft["version_num"] # Should exist, validated earlier
894-
if self._is_version_already_exists(entity_key, version_num):
895-
continue
896-
children = self._resolve_children(valid_draft, self.components_map_by_key)
897-
units_api.create_next_unit_version(
898-
self.units_map_by_key[entity_key],
899-
components=children,
900-
force_version_num=valid_draft.pop("version_num", None),
901-
created_by=self.user_id,
902-
**valid_draft
903-
)
904-
905-
for valid_draft in containers.get("subsection_drafts", []):
906-
entity_key = valid_draft.pop("entity_key")
907-
version_num = valid_draft["version_num"] # Should exist, validated earlier
908-
if self._is_version_already_exists(entity_key, version_num):
909-
continue
910-
children = self._resolve_children(valid_draft, self.units_map_by_key)
911-
subsections_api.create_next_subsection_version(
912-
self.subsections_map_by_key[entity_key],
913-
units=children,
914-
force_version_num=valid_draft.pop("version_num", None),
915-
created_by=self.user_id,
916-
**valid_draft
917-
)
892+
def _process_draft_containers(
893+
container_cls: containers_api.ContainerSubclass,
894+
container_map: dict,
895+
children_map: dict,
896+
):
897+
for valid_draft in containers.get(f"{container_cls.type_code}_drafts", []):
898+
entity_key = valid_draft.pop("entity_key")
899+
version_num = valid_draft["version_num"] # Should exist, validated earlier
900+
if self._is_version_already_exists(entity_key, version_num):
901+
continue
902+
children = self._resolve_children(valid_draft, children_map)
903+
del valid_draft["version_num"]
904+
containers_api.create_next_container_version(
905+
container_map[entity_key],
906+
**valid_draft, # should this be allowed to override any of the following fields?
907+
entities=children,
908+
force_version_num=version_num,
909+
created_by=self.user_id,
910+
)
918911

919-
for valid_draft in containers.get("section_drafts", []):
920-
entity_key = valid_draft.pop("entity_key")
921-
version_num = valid_draft["version_num"] # Should exist, validated earlier
922-
if self._is_version_already_exists(entity_key, version_num):
923-
continue
924-
children = self._resolve_children(valid_draft, self.subsections_map_by_key)
925-
sections_api.create_next_section_version(
926-
self.sections_map_by_key[entity_key],
927-
subsections=children,
928-
force_version_num=valid_draft.pop("version_num", None),
929-
created_by=self.user_id,
930-
**valid_draft
931-
)
912+
_process_draft_containers(Unit, self.units_map_by_key, children_map=self.components_map_by_key)
913+
_process_draft_containers(Subsection, self.subsections_map_by_key, children_map=self.units_map_by_key)
914+
_process_draft_containers(Section, self.sections_map_by_key, children_map=self.subsections_map_by_key)
932915

933916
# --------------------------
934917
# Utilities

src/openedx_content/applets/collections/api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"get_collection",
2525
"get_collections",
2626
"get_entity_collections",
27+
"get_collection_entities",
2728
"remove_from_collection",
2829
"restore_collection",
2930
"update_collection",
@@ -195,6 +196,18 @@ def get_entity_collections(learning_package_id: int, entity_key: str) -> QuerySe
195196
return entity.collections.filter(enabled=True).order_by("pk")
196197

197198

199+
def get_collection_entities(learning_package_id: int, collection_key: str) -> QuerySet[PublishableEntity]:
200+
"""
201+
Returns a QuerySet of PublishableEntities in a Collection.
202+
203+
This is the same as `collection.entities.all()`
204+
"""
205+
return PublishableEntity.objects.filter(
206+
learning_package_id=learning_package_id,
207+
collections__key=collection_key,
208+
).order_by("pk")
209+
210+
198211
def get_collections(learning_package_id: int, enabled: bool | None = True) -> QuerySet[Collection]:
199212
"""
200213
Get all collections for a given learning package

src/openedx_content/applets/components/models.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,16 @@ class ComponentType(models.Model):
6464
# the UsageKey.
6565
name = case_sensitive_char_field(max_length=100, blank=True)
6666

67-
# TODO: this needs to go into a class Meta
68-
constraints = [
69-
models.UniqueConstraint(
70-
fields=[
71-
"namespace",
72-
"name",
73-
],
74-
name="oel_component_type_uniq_ns_n",
75-
),
76-
]
67+
class Meta:
68+
constraints = [
69+
models.UniqueConstraint(
70+
fields=[
71+
"namespace",
72+
"name",
73+
],
74+
name="oel_component_type_uniq_ns_n",
75+
),
76+
]
7777

7878
def __str__(self) -> str:
7979
return f"{self.namespace}:{self.name}"

src/openedx_content/applets/containers/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)