Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9225c68
fix: add missing constraint to ComponentType
bradenmacdonald Mar 2, 2026
c6a9e0d
docs: remove outdated comment
bradenmacdonald Mar 2, 2026
5d781a6
feat: add new ContainerType model
bradenmacdonald Mar 3, 2026
1b9697a
test: add concrete TestContainer model
bradenmacdonald Mar 4, 2026
3d867fc
feat: consolidate containers implementation to use generic models only
bradenmacdonald Mar 6, 2026
0fc800c
test: revert "test: add concrete TestContainer model"
bradenmacdonald Mar 10, 2026
cd2e1a6
chore: update the rest to match
bradenmacdonald Mar 10, 2026
bc3e931
refactor: Unit -> UnitType, etc.
bradenmacdonald Mar 11, 2026
663b343
chore: quality issues
bradenmacdonald Mar 11, 2026
14a56bd
chore: quality issues
bradenmacdonald Mar 11, 2026
3bfd40a
chore: formatting
bradenmacdonald Mar 11, 2026
bb770e6
chore: format
bradenmacdonald Mar 11, 2026
161145c
WIP - restore the type-specific models
bradenmacdonald Mar 11, 2026
d80761e
WIP
bradenmacdonald Mar 12, 2026
6e8a63c
WIP
bradenmacdonald Mar 12, 2026
3f81e4d
WIP
bradenmacdonald Mar 12, 2026
9c335f6
WIP - refactoring test cases
bradenmacdonald Mar 12, 2026
8677456
WIP - refactoring test cases
bradenmacdonald Mar 12, 2026
d3cdf75
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
e44661b
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
84d565b
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
2a39de0
fix: learning package ID seems always to be required in this fn
bradenmacdonald Mar 13, 2026
8cbe7b0
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
1a1288e
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
2d2b078
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
b830f39
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
0dc4258
WIP - refactoring test cases
bradenmacdonald Mar 13, 2026
028907c
WIP - refactoring test cases
bradenmacdonald Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/openedx_content/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,3 @@
from .applets.components.admin import *
from .applets.media.admin import *
from .applets.publishing.admin import *
from .applets.sections.admin import *
from .applets.subsections.admin import *
from .applets.units.admin import *
8 changes: 4 additions & 4 deletions src/openedx_content/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"""

# These wildcard imports are okay because these api modules declare __all__.
# pylint: disable=wildcard-import
# pylint: disable=wildcard-import, unused-import
from .applets.backup_restore.api import *
from .applets.collections.api import *
from .applets.components.api import *
from .applets.media.api import *
from .applets.publishing.api import *
from .applets.sections.api import *
from .applets.subsections.api import *
from .applets.units.api import *
# from .applets.sections.api import *
# from .applets.subsections.api import *
# from .applets.units.api import *
168 changes: 75 additions & 93 deletions src/openedx_content/applets/backup_restore/zipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
from ..components import api as components_api
from ..media import api as media_api
from ..publishing import api as publishing_api
from ..sections import api as sections_api
from ..subsections import api as subsections_api
from ..units import api as units_api
from ..units.models import Unit
from ..subsections.models import Subsection
from ..sections.models import Section
from .serializers import (
CollectionSerializer,
ComponentSerializer,
Expand Down Expand Up @@ -804,70 +804,70 @@ def _save_components(self, learning_package, components, component_static_files)
**valid_published
)

def _save_units(self, learning_package, containers):
"""Save units and published unit versions."""
for valid_unit in containers.get("unit", []):
entity_key = valid_unit.get("key")
unit = units_api.create_unit(learning_package.id, created_by=self.user_id, **valid_unit)
self.units_map_by_key[entity_key] = unit
def _save_container(
self,
learning_package,
containers,
*,
container_type: publishing_api.ContainerType,
container_map: dict,
children_map: dict,
):
"""Internal logic for _save_units, _save_subsections, and _save_sections"""
type_code = container_type.type_code # e.g. "unit"
for data in containers.get(type_code, []):
entity_key = data.get("key")
container = publishing_api.create_container(
learning_package.id,
**data, # should this be allowed to override any of the following fields?
created_by=self.user_id,
container_type=container_type,
)
container_map[entity_key] = container # e.g. `self.units_map_by_key[entity_key] = unit`

for valid_published in containers.get("unit_published", []):
for valid_published in containers.get(f"{type_code}_published", []):
entity_key = valid_published.pop("entity_key")
children = self._resolve_children(valid_published, self.components_map_by_key)
children = self._resolve_children(valid_published, children_map)
self.all_published_entities_versions.add(
(entity_key, valid_published.get('version_num'))
) # Track published version
units_api.create_next_unit_version(
self.units_map_by_key[entity_key],
publishing_api.create_next_container_version(
container_map[entity_key],
**valid_published, # should this be allowed to override any of the following fields?
force_version_num=valid_published.pop("version_num", None),
components=children,
entities=children,
created_by=self.user_id,
**valid_published
)

def _save_units(self, learning_package, containers):
"""Save units and published unit versions."""
self._save_container(
learning_package,
containers,
container_type=Unit,
container_map=self.units_map_by_key,
children_map=self.components_map_by_key,
)

def _save_subsections(self, learning_package, containers):
"""Save subsections and published subsection versions."""
for valid_subsection in containers.get("subsection", []):
entity_key = valid_subsection.get("key")
subsection = subsections_api.create_subsection(
learning_package.id, created_by=self.user_id, **valid_subsection
)
self.subsections_map_by_key[entity_key] = subsection

for valid_published in containers.get("subsection_published", []):
entity_key = valid_published.pop("entity_key")
children = self._resolve_children(valid_published, self.units_map_by_key)
self.all_published_entities_versions.add(
(entity_key, valid_published.get('version_num'))
) # Track published version
subsections_api.create_next_subsection_version(
self.subsections_map_by_key[entity_key],
units=children,
force_version_num=valid_published.pop("version_num", None),
created_by=self.user_id,
**valid_published
)
self._save_container(
learning_package,
containers,
container_type=Subsection,
container_map=self.subsections_map_by_key,
children_map=self.units_map_by_key,
)

def _save_sections(self, learning_package, containers):
"""Save sections and published section versions."""
for valid_section in containers.get("section", []):
entity_key = valid_section.get("key")
section = sections_api.create_section(learning_package.id, created_by=self.user_id, **valid_section)
self.sections_map_by_key[entity_key] = section

for valid_published in containers.get("section_published", []):
entity_key = valid_published.pop("entity_key")
children = self._resolve_children(valid_published, self.subsections_map_by_key)
self.all_published_entities_versions.add(
(entity_key, valid_published.get('version_num'))
) # Track published version
sections_api.create_next_section_version(
self.sections_map_by_key[entity_key],
subsections=children,
force_version_num=valid_published.pop("version_num", None),
created_by=self.user_id,
**valid_published
)
self._save_container(
learning_package,
containers,
container_type=Section,
container_map=self.sections_map_by_key,
children_map=self.subsections_map_by_key,
)

def _save_draft_versions(self, components, containers, component_static_files):
"""Save draft versions for all entity types."""
Expand All @@ -888,47 +888,29 @@ def _save_draft_versions(self, components, containers, component_static_files):
**valid_draft
)

for valid_draft in containers.get("unit_drafts", []):
entity_key = valid_draft.pop("entity_key")
version_num = valid_draft["version_num"] # Should exist, validated earlier
if self._is_version_already_exists(entity_key, version_num):
continue
children = self._resolve_children(valid_draft, self.components_map_by_key)
units_api.create_next_unit_version(
self.units_map_by_key[entity_key],
components=children,
force_version_num=valid_draft.pop("version_num", None),
created_by=self.user_id,
**valid_draft
)

for valid_draft in containers.get("subsection_drafts", []):
entity_key = valid_draft.pop("entity_key")
version_num = valid_draft["version_num"] # Should exist, validated earlier
if self._is_version_already_exists(entity_key, version_num):
continue
children = self._resolve_children(valid_draft, self.units_map_by_key)
subsections_api.create_next_subsection_version(
self.subsections_map_by_key[entity_key],
units=children,
force_version_num=valid_draft.pop("version_num", None),
created_by=self.user_id,
**valid_draft
)
def _process_draft_containers(
container_type: publishing_api.ContainerType,
container_map: dict,
children_map: dict,
):
for valid_draft in containers.get(f"{container_type.type_code}_drafts", []):
entity_key = valid_draft.pop("entity_key")
version_num = valid_draft["version_num"] # Should exist, validated earlier
if self._is_version_already_exists(entity_key, version_num):
continue
children = self._resolve_children(valid_draft, children_map)
del valid_draft["version_num"]
publishing_api.create_next_container_version(
container_map[entity_key],
**valid_draft, # should this be allowed to override any of the following fields?
entities=children,
force_version_num=version_num,
created_by=self.user_id,
)

for valid_draft in containers.get("section_drafts", []):
entity_key = valid_draft.pop("entity_key")
version_num = valid_draft["version_num"] # Should exist, validated earlier
if self._is_version_already_exists(entity_key, version_num):
continue
children = self._resolve_children(valid_draft, self.subsections_map_by_key)
sections_api.create_next_section_version(
self.sections_map_by_key[entity_key],
subsections=children,
force_version_num=valid_draft.pop("version_num", None),
created_by=self.user_id,
**valid_draft
)
_process_draft_containers(Unit, self.units_map_by_key, children_map=self.components_map_by_key)
_process_draft_containers(Subsection, self.subsections_map_by_key, children_map=self.units_map_by_key)
_process_draft_containers(Section, self.sections_map_by_key, children_map=self.subsections_map_by_key)

# --------------------------
# Utilities
Expand Down
15 changes: 14 additions & 1 deletion src/openedx_content/applets/collections/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.db.models import QuerySet

from ..publishing import api as publishing_api
from ..publishing.models import PublishableEntity
from ..publishing.models import Container, PublishableEntity
from .models import Collection, CollectionPublishableEntity

# The public API that will be re-exported by openedx_content.api
Expand All @@ -24,6 +24,7 @@
"get_collection",
"get_collections",
"get_entity_collections",
"get_collection_containers",
"remove_from_collection",
"restore_collection",
"update_collection",
Expand Down Expand Up @@ -195,6 +196,18 @@ def get_entity_collections(learning_package_id: int, entity_key: str) -> QuerySe
return entity.collections.filter(enabled=True).order_by("pk")


def get_collection_containers(learning_package_id: int, collection_key: str) -> QuerySet[Container]:
"""
Returns a QuerySet of Containers relating to the PublishableEntities in a Collection.

Containers have a one-to-one relationship with PublishableEntity, but the reverse may not always be true.
"""
return Container.objects.filter(
publishable_entity__learning_package_id=learning_package_id,
publishable_entity__collections__key=collection_key,
).order_by("pk")


def get_collections(learning_package_id: int, enabled: bool | None = True) -> QuerySet[Collection]:
"""
Get all collections for a given learning package
Expand Down
20 changes: 10 additions & 10 deletions src/openedx_content/applets/components/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ class ComponentType(models.Model):
# the UsageKey.
name = case_sensitive_char_field(max_length=100, blank=True)

# TODO: this needs to go into a class Meta
constraints = [
models.UniqueConstraint(
fields=[
"namespace",
"name",
],
name="oel_component_type_uniq_ns_n",
),
]
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"namespace",
"name",
],
name="oel_component_type_uniq_ns_n",
),
]

def __str__(self) -> str:
return f"{self.namespace}:{self.name}"
Expand Down
Loading
Loading