Skip to content

Commit 4972f43

Browse files
refactor: Units inherit Containers (Multi-Table Inheritance)
1 parent f666ba4 commit 4972f43

10 files changed

Lines changed: 177 additions & 272 deletions

File tree

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 33 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,13 @@
88

99
from dataclasses import dataclass
1010
from datetime import datetime, timezone
11+
from typing import TypeVar
1112

1213
from django.core.exceptions import ObjectDoesNotExist, ValidationError
1314
from django.db.models import F, Q, QuerySet
1415
from django.db.transaction import atomic
1516

16-
from .model_mixins import (
17-
ContainerMixin,
18-
PublishableContentModelRegistry,
19-
PublishableEntityMixin,
20-
PublishableEntityVersionMixin,
21-
)
17+
from .model_mixins import PublishableContentModelRegistry, PublishableEntityMixin, PublishableEntityVersionMixin
2218
from .models import (
2319
Container,
2420
ContainerVersion,
@@ -33,6 +29,9 @@
3329
PublishLogRecord,
3430
)
3531

32+
ContainerModel = TypeVar('ContainerModel', bound=Container)
33+
ContainerVersionModel = TypeVar('ContainerVersionModel', bound=ContainerVersion)
34+
3635
# The public API that will be re-exported by openedx_learning.apps.authoring.api
3736
# is listed in the __all__ entries below. Internal helper functions that are
3837
# private to this module should start with an underscore. If a function does not
@@ -66,7 +65,6 @@
6665
"create_container",
6766
"create_container_version",
6867
"create_next_container_version",
69-
"create_container_and_version",
7068
"get_container",
7169
"ContainerEntityListEntry",
7270
"get_entities_in_draft_container",
@@ -581,7 +579,9 @@ def create_container(
581579
key: str,
582580
created: datetime,
583581
created_by: int | None,
584-
) -> Container:
582+
# The types on the following line are correct, but mypy will complain - https://github.com/python/mypy/issues/3737
583+
container_model: type[ContainerModel] = Container, # type: ignore[assignment]
584+
) -> ContainerModel:
585585
"""
586586
[ 🛑 UNSTABLE ]
587587
Create a new container.
@@ -591,15 +591,17 @@ def create_container(
591591
key: The key of the container.
592592
created: The date and time the container was created.
593593
created_by: The ID of the user who created the container
594+
container_model: The subclass of Container to use, if applicable
594595
595596
Returns:
596597
The newly created container.
597598
"""
599+
assert issubclass(container_model, Container)
598600
with atomic():
599601
publishable_entity = create_publishable_entity(
600602
learning_package_id, key, created, created_by
601603
)
602-
container = Container.objects.create(
604+
container = container_model.objects.create(
603605
publishable_entity=publishable_entity,
604606
)
605607
return container
@@ -635,7 +637,7 @@ def create_entity_list_with_rows(
635637
The newly created entity list.
636638
"""
637639
order_nums = range(len(entity_pks))
638-
with atomic():
640+
with atomic(savepoint=False):
639641
entity_list = create_entity_list()
640642
EntityListRow.objects.bulk_create(
641643
[
@@ -662,7 +664,8 @@ def create_container_version(
662664
entity_version_pks: list[int | None] | None,
663665
created: datetime,
664666
created_by: int | None,
665-
) -> ContainerVersion:
667+
container_version_model: type[ContainerVersionModel] = ContainerVersion, # type: ignore[assignment]
668+
) -> ContainerVersionModel:
666669
"""
667670
[ 🛑 UNSTABLE ]
668671
Create a new container version.
@@ -675,11 +678,13 @@ def create_container_version(
675678
entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
676679
created: The date and time the container version was created.
677680
created_by: The ID of the user who created the container version.
681+
container_version_model: The subclass of ContainerVersion to use, if applicable.
678682
679683
Returns:
680684
The newly created container version.
681685
"""
682-
with atomic():
686+
assert issubclass(container_version_model, ContainerVersion)
687+
with atomic(savepoint=False):
683688
container = Container.objects.select_related("publishable_entity").get(pk=container_pk)
684689
entity = container.publishable_entity
685690

@@ -706,7 +711,7 @@ def create_container_version(
706711
created=created,
707712
created_by=created_by,
708713
)
709-
container_version = ContainerVersion.objects.create(
714+
container_version = container_version_model.objects.create(
710715
publishable_entity_version=publishable_entity_version,
711716
container_id=container_pk,
712717
entity_list=entity_list,
@@ -723,7 +728,8 @@ def create_next_container_version(
723728
entity_version_pks: list[int | None] | None,
724729
created: datetime,
725730
created_by: int | None,
726-
) -> ContainerVersion:
731+
container_version_model: type[ContainerVersionModel] = ContainerVersion, # type: ignore[assignment]
732+
) -> ContainerVersionModel:
727733
"""
728734
[ 🛑 UNSTABLE ]
729735
Create the next version of a container. A new version of the container is created
@@ -742,10 +748,12 @@ def create_next_container_version(
742748
entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
743749
created: The date and time the container version was created.
744750
created_by: The ID of the user who created the container version.
751+
container_version_model: The subclass of ContainerVersion to use, if applicable.
745752
746753
Returns:
747754
The newly created container version.
748755
"""
756+
assert issubclass(container_version_model, ContainerVersion)
749757
with atomic():
750758
container = Container.objects.select_related("publishable_entity").get(pk=container_pk)
751759
entity = container.publishable_entity
@@ -776,7 +784,7 @@ def create_next_container_version(
776784
created=created,
777785
created_by=created_by,
778786
)
779-
next_container_version = ContainerVersion.objects.create(
787+
next_container_version = container_version_model.objects.create(
780788
publishable_entity_version=publishable_entity_version,
781789
container_id=container_pk,
782790
entity_list=next_entity_list,
@@ -785,46 +793,6 @@ def create_next_container_version(
785793
return next_container_version
786794

787795

788-
def create_container_and_version(
789-
learning_package_id: int,
790-
key: str,
791-
*,
792-
created: datetime,
793-
created_by: int | None,
794-
title: str,
795-
publishable_entities_pks: list[int],
796-
entity_version_pks: list[int | None],
797-
) -> tuple[Container, ContainerVersion]:
798-
"""
799-
[ 🛑 UNSTABLE ]
800-
Create a new container and its first version.
801-
802-
Args:
803-
learning_package_id: The ID of the learning package that contains the container.
804-
key: The key of the container.
805-
created: The date and time the container was created.
806-
created_by: The ID of the user who created the container.
807-
version_num: The version number of the container.
808-
title: The title of the container.
809-
members_pk: The IDs of the members of the container.
810-
811-
Returns:
812-
The newly created container version.
813-
"""
814-
with atomic():
815-
container = create_container(learning_package_id, key, created, created_by)
816-
container_version = create_container_version(
817-
container.publishable_entity.pk,
818-
1,
819-
title=title,
820-
publishable_entities_pks=publishable_entities_pks,
821-
entity_version_pks=entity_version_pks,
822-
created=created,
823-
created_by=created_by,
824-
)
825-
return (container, container_version)
826-
827-
828796
def get_container(pk: int) -> Container:
829797
"""
830798
[ 🛑 UNSTABLE ]
@@ -854,15 +822,13 @@ def entity(self):
854822

855823

856824
def get_entities_in_draft_container(
857-
container: Container | ContainerMixin,
825+
container: Container,
858826
) -> list[ContainerEntityListEntry]:
859827
"""
860828
[ 🛑 UNSTABLE ]
861829
Get the list of entities and their versions in the draft version of the
862830
given container.
863831
"""
864-
if isinstance(container, ContainerMixin):
865-
container = container.container
866832
assert isinstance(container, Container)
867833
entity_list = []
868834
for row in container.versioning.draft.entity_list.entitylistrow_set.order_by("order_num"):
@@ -877,19 +843,15 @@ def get_entities_in_draft_container(
877843

878844

879845
def get_entities_in_published_container(
880-
container: Container | ContainerMixin,
846+
container: Container,
881847
) -> list[ContainerEntityListEntry] | None:
882848
"""
883849
[ 🛑 UNSTABLE ]
884850
Get the list of entities and their versions in the published version of the
885851
given container.
886852
"""
887-
if isinstance(container, ContainerMixin):
888-
cv = container.container.versioning.published
889-
elif isinstance(container, Container):
890-
cv = container.versioning.published
891-
else:
892-
raise TypeError(f"Expected Container or ContainerMixin; got {type(container)}")
853+
assert isinstance(container, Container)
854+
cv = container.versioning.published
893855
if cv is None:
894856
return None # There is no published version of this container. Should this be an exception?
895857
assert isinstance(cv, ContainerVersion)
@@ -905,9 +867,7 @@ def get_entities_in_published_container(
905867
return entity_list
906868

907869

908-
def contains_unpublished_changes(
909-
container: Container | ContainerMixin,
910-
) -> bool:
870+
def contains_unpublished_changes(container_id: int) -> bool:
911871
"""
912872
[ 🛑 UNSTABLE ]
913873
Check recursively if a container has any unpublished changes.
@@ -920,14 +880,10 @@ def contains_unpublished_changes(
920880
that's in the container, it will be `False`. This method will return `True`
921881
in either case.
922882
"""
923-
if isinstance(container, ContainerMixin):
924-
# This is similar to 'get_container(container.container_id)' but pre-loads more data.
925-
container = Container.objects.select_related(
926-
"publishable_entity__draft__version__containerversion__entity_list",
927-
).get(pk=container.container_id)
928-
else:
929-
pass # TODO: select_related if we're given a raw Container rather than a ContainerMixin like Unit?
930-
assert isinstance(container, Container)
883+
# This is similar to 'get_container(container.container_id)' but pre-loads more data.
884+
container = Container.objects.select_related(
885+
"publishable_entity__draft__version__containerversion__entity_list",
886+
).get(pk=container_id)
931887

932888
if container.versioning.has_unpublished_changes:
933889
return True
@@ -949,7 +905,7 @@ def contains_unpublished_changes(
949905
child_container = None
950906
if child_container:
951907
# This is itself a container - check recursively:
952-
if contains_unpublished_changes(child_container):
908+
if contains_unpublished_changes(child_container.pk):
953909
return True
954910
else:
955911
# This is not a container:

openedx_learning/apps/authoring/publishing/migrations/0003_containers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.19 on 2025-03-07 23:09
1+
# Generated by Django 4.2.19 on 2025-03-11 04:10
22

33
import django.db.models.deletion
44
from django.db import migrations, models
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""
22
Mixins provided by the publishing app
33
"""
4-
from .container import *
54
from .publishable_entity import *

openedx_learning/apps/authoring/publishing/model_mixins/container.py

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)