Skip to content

Commit 98b8d15

Browse files
navinkarkeraormsbee
authored andcommitted
feat: add/remove/update children to container api
Modify children components of a container via API. It is possible to move add and remove children section to edx-platform but it would require additional call to fetch existing children from database before calculating new children list.
1 parent 21c7ec6 commit 98b8d15

File tree

5 files changed

+198
-9
lines changed

5 files changed

+198
-9
lines changed

openedx_learning/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Open edX Learning ("Learning Core").
33
"""
44

5-
__version__ = "0.19.1"
5+
__version__ = "0.19.2"

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

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

1314
from django.core.exceptions import ObjectDoesNotExist, ValidationError
@@ -73,10 +74,12 @@
7374
"get_container",
7475
"get_container_by_key",
7576
"get_containers",
77+
"ChildrenEntitiesAction",
7678
"ContainerEntityListEntry",
7779
"get_entities_in_container",
7880
"contains_unpublished_changes",
7981
"get_containers_with_entity",
82+
"get_container_children_count",
8083
]
8184

8285

@@ -771,6 +774,70 @@ def create_container_version(
771774
return container_version
772775

773776

777+
class ChildrenEntitiesAction(Enum):
778+
"""Possible actions for children entities"""
779+
780+
APPEND = "append"
781+
REMOVE = "remove"
782+
REPLACE = "replace"
783+
784+
785+
def create_next_entity_list(
786+
learning_package_id: int,
787+
last_version: ContainerVersion,
788+
publishable_entities_pks: list[int],
789+
entity_version_pks: list[int | None] | None,
790+
entities_action: ChildrenEntitiesAction = ChildrenEntitiesAction.REPLACE,
791+
) -> EntityList:
792+
"""
793+
Creates next entity list based on the given entities_action.
794+
795+
Args:
796+
learning_package_id: Learning package ID
797+
last_version: Last version of container.
798+
publishable_entities_pks: The IDs of the members current members of the container.
799+
entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
800+
entities_action: APPEND, REMOVE or REPLACE given entities from/to the container
801+
802+
Returns:
803+
The newly created entity list.
804+
"""
805+
if entity_version_pks is None:
806+
entity_version_pks: list[int | None] = [None] * len(publishable_entities_pks) # type: ignore[no-redef]
807+
if entities_action == ChildrenEntitiesAction.APPEND:
808+
# get previous entity list rows
809+
last_entities = last_version.entity_list.entitylistrow_set.only(
810+
"entity_id",
811+
"entity_version_id"
812+
).order_by("order_num")
813+
# append given publishable_entities_pks and entity_version_pks
814+
publishable_entities_pks = [entity.entity_id for entity in last_entities] + publishable_entities_pks
815+
entity_version_pks = [ # type: ignore[operator, assignment]
816+
entity.entity_version_id
817+
for entity in last_entities
818+
] + entity_version_pks
819+
elif entities_action == ChildrenEntitiesAction.REMOVE:
820+
# get previous entity list rows
821+
last_entities = last_version.entity_list.entitylistrow_set.only(
822+
"entity_id",
823+
"entity_version_id"
824+
).order_by("order_num")
825+
# Remove entities that are in publishable_entities_pks
826+
new_entities = [
827+
entity
828+
for entity in last_entities
829+
if entity.entity_id not in publishable_entities_pks
830+
]
831+
publishable_entities_pks = [entity.entity_id for entity in new_entities]
832+
entity_version_pks = [entity.entity_version_id for entity in new_entities]
833+
next_entity_list = create_entity_list_with_rows(
834+
entity_pks=publishable_entities_pks,
835+
entity_version_pks=entity_version_pks, # type: ignore[arg-type]
836+
learning_package_id=learning_package_id,
837+
)
838+
return next_entity_list
839+
840+
774841
def create_next_container_version(
775842
container_pk: int,
776843
*,
@@ -780,6 +847,7 @@ def create_next_container_version(
780847
created: datetime,
781848
created_by: int | None,
782849
container_version_cls: type[ContainerVersionModel] = ContainerVersion, # type: ignore[assignment]
850+
entities_action: ChildrenEntitiesAction = ChildrenEntitiesAction.REPLACE,
783851
) -> ContainerVersionModel:
784852
"""
785853
[ 🛑 UNSTABLE ]
@@ -815,13 +883,14 @@ def create_next_container_version(
815883
# We're only changing metadata. Keep the same entity list.
816884
next_entity_list = last_version.entity_list
817885
else:
818-
if entity_version_pks is None:
819-
entity_version_pks = [None] * len(publishable_entities_pks)
820-
next_entity_list = create_entity_list_with_rows(
821-
entity_pks=publishable_entities_pks,
822-
entity_version_pks=entity_version_pks,
823-
learning_package_id=entity.learning_package_id,
886+
next_entity_list = create_next_entity_list(
887+
entity.learning_package_id,
888+
last_version,
889+
publishable_entities_pks,
890+
entity_version_pks,
891+
entities_action
824892
)
893+
825894
next_container_version = _create_container_version(
826895
container,
827896
next_version_num,
@@ -1018,3 +1087,29 @@ def get_containers_with_entity(
10181087
# publishable_entity__draft__version__containerversion__entity_list__in=lists
10191088
# )
10201089
return qs
1090+
1091+
1092+
def get_container_children_count(
1093+
container: Container,
1094+
*,
1095+
published: bool,
1096+
):
1097+
"""
1098+
[ 🛑 UNSTABLE ]
1099+
Get the count of entities in the current draft or published version of the given container.
1100+
1101+
Args:
1102+
container: The Container, e.g. returned by `get_container()`
1103+
published: `True` if we want the published version of the container, or
1104+
`False` for the draft version.
1105+
"""
1106+
assert isinstance(container, Container)
1107+
container_version = container.versioning.published if published else container.versioning.draft
1108+
if container_version is None:
1109+
raise ContainerVersion.DoesNotExist # This container has not been published yet, or has been deleted.
1110+
assert isinstance(container_version, ContainerVersion)
1111+
if published:
1112+
filter_deleted = {"entity__published__version__isnull": False}
1113+
else:
1114+
filter_deleted = {"entity__draft__version__isnull": False}
1115+
return container_version.entity_list.entitylistrow_set.filter(**filter_deleted).count()

openedx_learning/apps/authoring/publishing/models/entity_list.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class EntityListRow(models.Model):
6060
)
6161

6262
class Meta:
63+
ordering = ["order_num"]
6364
constraints = [
6465
# If (entity_list, order_num) is not unique, it likely indicates a race condition - so force uniqueness.
6566
models.UniqueConstraint(

openedx_learning/apps/authoring/units/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def create_next_unit_version(
130130
components: list[Component | ComponentVersion] | None = None,
131131
created: datetime,
132132
created_by: int | None = None,
133+
entities_action: publishing_api.ChildrenEntitiesAction = publishing_api.ChildrenEntitiesAction.REPLACE,
133134
) -> UnitVersion:
134135
"""
135136
[ 🛑 UNSTABLE ] Create the next unit version.
@@ -151,6 +152,7 @@ def create_next_unit_version(
151152
created=created,
152153
created_by=created_by,
153154
container_version_cls=UnitVersion,
155+
entities_action=entities_action,
154156
)
155157
return unit_version
156158

tests/openedx_learning/apps/authoring/units/test_api.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ def test_add_component_after_publish(self):
412412
components=[self.component_1],
413413
created=self.now,
414414
created_by=None,
415+
entities_action=authoring_api.ChildrenEntitiesAction.APPEND,
415416
)
416417
# Now the unit should have unpublished changes:
417418
unit.refresh_from_db() # Reloading the unit is necessary
@@ -697,8 +698,9 @@ def test_removing_component(self):
697698
authoring_api.create_next_unit_version(
698699
unit=unit,
699700
title="Revised with component 2 deleted",
700-
components=[self.component_1], # component 2 is gone
701+
components=[self.component_2],
701702
created=self.now,
703+
entities_action=authoring_api.ChildrenEntitiesAction.REMOVE,
702704
)
703705

704706
# Now it should not be listed in the unit:
@@ -768,8 +770,9 @@ def test_soft_deleting_and_removing_component(self):
768770
authoring_api.create_next_unit_version(
769771
unit=unit,
770772
title="Revised with component 2 deleted",
771-
components=[self.component_1],
773+
components=[self.component_2],
772774
created=self.now,
775+
entities_action=authoring_api.ChildrenEntitiesAction.REMOVE,
773776
)
774777

775778
# Now it should not be listed in the unit:
@@ -965,6 +968,94 @@ def test_units_containing(self):
965968
]
966969
assert result2 == [unit4_unpinned]
967970

971+
def test_add_remove_container_children(self):
972+
"""
973+
Test adding and removing children components from containers.
974+
"""
975+
unit, unit_version = authoring_api.create_unit_and_version(
976+
learning_package_id=self.learning_package.id,
977+
key="unit:key",
978+
title="Unit",
979+
components=[self.component_1],
980+
created=self.now,
981+
created_by=None,
982+
)
983+
assert authoring_api.get_components_in_unit(unit, published=False) == [
984+
Entry(self.component_1.versioning.draft),
985+
]
986+
component_3, _ = self.create_component(
987+
key="Query Counting (3)",
988+
title="Querying Counting Problem (3)",
989+
)
990+
# Add component_2 and component_3
991+
unit_version_v2 = authoring_api.create_next_unit_version(
992+
unit=unit,
993+
title=unit_version.title,
994+
components=[self.component_2, component_3],
995+
created=self.now,
996+
created_by=None,
997+
entities_action=authoring_api.ChildrenEntitiesAction.APPEND,
998+
)
999+
unit.refresh_from_db()
1000+
assert unit_version_v2.version_num == 2
1001+
assert unit_version_v2 in unit.versioning.versions.all()
1002+
# Verify that component_2 and component_3 is added to end
1003+
assert authoring_api.get_components_in_unit(unit, published=False) == [
1004+
Entry(self.component_1.versioning.draft),
1005+
Entry(self.component_2.versioning.draft),
1006+
Entry(component_3.versioning.draft),
1007+
]
1008+
1009+
# Remove component_1
1010+
authoring_api.create_next_unit_version(
1011+
unit=unit,
1012+
title=unit_version.title,
1013+
components=[self.component_1],
1014+
created=self.now,
1015+
created_by=None,
1016+
entities_action=authoring_api.ChildrenEntitiesAction.REMOVE,
1017+
)
1018+
unit.refresh_from_db()
1019+
# Verify that component_1 is removed
1020+
assert authoring_api.get_components_in_unit(unit, published=False) == [
1021+
Entry(self.component_2.versioning.draft),
1022+
Entry(component_3.versioning.draft),
1023+
]
1024+
1025+
def test_get_container_children_count(self):
1026+
"""
1027+
Test get_container_children_count()
1028+
"""
1029+
unit = self.create_unit_with_components([self.component_1])
1030+
assert authoring_api.get_container_children_count(unit.container, published=False) == 1
1031+
# publish
1032+
authoring_api.publish_all_drafts(self.learning_package.id)
1033+
unit_version = unit.versioning.draft
1034+
authoring_api.create_next_unit_version(
1035+
unit=unit,
1036+
title=unit_version.title,
1037+
components=[self.component_2],
1038+
created=self.now,
1039+
created_by=None,
1040+
entities_action=authoring_api.ChildrenEntitiesAction.APPEND,
1041+
)
1042+
unit.refresh_from_db()
1043+
# Should have two components in draft version and 1 in published version
1044+
assert authoring_api.get_container_children_count(unit.container, published=False) == 2
1045+
assert authoring_api.get_container_children_count(unit.container, published=True) == 1
1046+
# publish
1047+
authoring_api.publish_all_drafts(self.learning_package.id)
1048+
unit.refresh_from_db()
1049+
assert authoring_api.get_container_children_count(unit.container, published=True) == 2
1050+
# Soft delete component_1
1051+
authoring_api.soft_delete_draft(self.component_1.pk)
1052+
unit.refresh_from_db()
1053+
# Should contain only 1 child
1054+
assert authoring_api.get_container_children_count(unit.container, published=False) == 1
1055+
authoring_api.publish_all_drafts(self.learning_package.id)
1056+
unit.refresh_from_db()
1057+
assert authoring_api.get_container_children_count(unit.container, published=True) == 1
1058+
9681059
# Tests TODO:
9691060
# Test that I can get a [PublishLog] history of a given unit and all its children, including children that aren't
9701061
# currently in the unit and excluding children that are only in other units.

0 commit comments

Comments
 (0)