Skip to content

Commit 12c6d3a

Browse files
feat: allow changing container title/metadata without changing its list
1 parent b58662e commit 12c6d3a

File tree

3 files changed

+86
-70
lines changed

3 files changed

+86
-70
lines changed

openedx_learning/apps/authoring/containers/api.py

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -110,44 +110,13 @@ def create_entity_list_with_rows(
110110
return entity_list
111111

112112

113-
def get_entity_list_with_pinned_versions(
114-
rows: QuerySet[EntityListRow],
115-
) -> EntityList:
116-
"""
117-
[ 🛑 UNSTABLE ]
118-
Copy rows from an existing entity list to a new entity list.
119-
120-
Args:
121-
entity_list: The entity list to copy the rows to.
122-
rows: The rows to copy to the new entity list.
123-
124-
Returns:
125-
The newly created entity list.
126-
"""
127-
entity_list = create_entity_list()
128-
with atomic():
129-
_ = EntityListRow.objects.bulk_create(
130-
[
131-
EntityListRow(
132-
entity_list=entity_list,
133-
entity_id=row.entity.id,
134-
order_num=row.order_num,
135-
entity_version_id=None, # For simplicity, we are not copying the pinned versions
136-
)
137-
for row in rows
138-
]
139-
)
140-
141-
return entity_list
142-
143-
144113
def create_container_version(
145114
container_pk: int,
146115
version_num: int,
147116
*,
148117
title: str,
149118
publishable_entities_pks: list[int],
150-
entity_version_pks: list[int | None],
119+
entity_version_pks: list[int | None] | None,
151120
created: datetime,
152121
created_by: int | None,
153122
) -> ContainerEntityVersion:
@@ -169,31 +138,45 @@ def create_container_version(
169138
with atomic():
170139
container = ContainerEntity.objects.select_related("publishable_entity").get(pk=container_pk)
171140
entity = container.publishable_entity
141+
142+
# Do a quick check that the given entities are in the right learning package:
143+
if PublishableEntity.objects.filter(
144+
pk__in=publishable_entities_pks,
145+
).exclude(
146+
learning_package_id=entity.learning_package_id,
147+
).exists():
148+
raise ValidationError("Container entities must be from the same learning package.")
149+
150+
assert title is not None
151+
assert publishable_entities_pks is not None
152+
if entity_version_pks is None:
153+
entity_version_pks = [None] * len(publishable_entities_pks)
154+
entity_list = create_entity_list_with_rows(
155+
entity_pks=publishable_entities_pks,
156+
entity_version_pks=entity_version_pks,
157+
)
172158
publishable_entity_version = publishing_api.create_publishable_entity_version(
173159
entity.pk,
174160
version_num=version_num,
175161
title=title,
176162
created=created,
177163
created_by=created_by,
178164
)
179-
entity_list = create_entity_list_with_rows(
180-
entity_pks=publishable_entities_pks,
181-
entity_version_pks=entity_version_pks,
182-
)
183165
container_version = ContainerEntityVersion.objects.create(
184166
publishable_entity_version=publishable_entity_version,
185167
container_id=container_pk,
186168
entity_list=entity_list,
187169
)
170+
188171
return container_version
189172

190173

191174
def create_next_container_version(
192175
container_pk: int,
193176
*,
194-
title: str,
195-
publishable_entities_pks: list[int],
196-
entity_version_pks: list[int | None],
177+
title: str | None,
178+
publishable_entities_pks: list[int] | None,
179+
entity_version_pks: list[int | None] | None,
197180
created: datetime,
198181
created_by: int | None,
199182
) -> ContainerEntityVersion:
@@ -210,37 +193,45 @@ def create_next_container_version(
210193
211194
Args:
212195
container_pk: The ID of the container to create the next version of.
213-
title: The title of the container.
214-
publishable_entities_pks: The IDs of the members current members of the container.
196+
title: The title of the container. None to keep the current title.
197+
publishable_entities_pks: The IDs of the members current members of the container. Or None for no change.
198+
entity_version_pks: The IDs of the versions to pin to, if pinning is desired.
215199
created: The date and time the container version was created.
216200
created_by: The ID of the user who created the container version.
217201
218202
Returns:
219203
The newly created container version.
220204
"""
221-
# Do a quick check that the given entities are in the right learning package:
222-
if PublishableEntity.objects.filter(
223-
pk__in=publishable_entities_pks,
224-
).exclude(
225-
learning_package_id=entity.learning_package_id,
226-
).exists():
227-
raise ValidationError("Container entities must be from the same learning package.")
228205
with atomic():
229206
container = ContainerEntity.objects.select_related("publishable_entity").get(pk=container_pk)
230207
entity = container.publishable_entity
231208
last_version = container.versioning.latest
209+
assert last_version is not None
232210
next_version_num = last_version.version_num + 1
211+
if publishable_entities_pks is None:
212+
# We're only changing metadata. Keep the same entity list.
213+
next_entity_list = last_version.entity_list
214+
else:
215+
# Do a quick check that the given entities are in the right learning package:
216+
if PublishableEntity.objects.filter(
217+
pk__in=publishable_entities_pks,
218+
).exclude(
219+
learning_package_id=entity.learning_package_id,
220+
).exists():
221+
raise ValidationError("Container entities must be from the same learning package.")
222+
if entity_version_pks is None:
223+
entity_version_pks = [None] * len(publishable_entities_pks)
224+
next_entity_list = create_entity_list_with_rows(
225+
entity_pks=publishable_entities_pks,
226+
entity_version_pks=entity_version_pks,
227+
)
233228
publishable_entity_version = publishing_api.create_publishable_entity_version(
234229
entity.pk,
235230
version_num=next_version_num,
236-
title=title,
231+
title=title if title is not None else last_version.title,
237232
created=created,
238233
created_by=created_by,
239234
)
240-
next_entity_list = create_entity_list_with_rows(
241-
entity_pks=publishable_entities_pks,
242-
entity_version_pks=entity_version_pks,
243-
)
244235
next_container_version = ContainerEntityVersion.objects.create(
245236
publishable_entity_version=publishable_entity_version,
246237
container_id=container_pk,

openedx_learning/apps/authoring/units/api.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ def create_unit_version(
9595

9696
def create_next_unit_version(
9797
unit: Unit,
98-
title: str,
99-
components: list[Component | ComponentVersion],
98+
*,
99+
title: str | None = None,
100+
components: list[Component | ComponentVersion] | None = None,
100101
created: datetime,
101102
created_by: int | None = None,
102103
) -> UnitVersion:
@@ -105,25 +106,29 @@ def create_next_unit_version(
105106
106107
Args:
107108
unit_pk: The unit ID.
108-
title: The title.
109-
components: The components, as a list of Components (unpinned) and/or ComponentVersions (pinned)
109+
title: The title. Leave as None to keep the current title.
110+
components: The components, as a list of Components (unpinned) and/or ComponentVersions (pinned). Passing None
111+
will leave the existing components unchanged.
110112
created: The creation date.
111113
created_by: The user who created the unit.
112114
"""
113-
for c in components:
114-
if not isinstance(c, (Component, ComponentVersion)):
115-
raise TypeError("Unit components must be either Component or ComponentVersion.")
116-
publishable_entities_pks = [
117-
(c.publishable_entity_id if isinstance(c, Component) else c.component.publishable_entity_id)
118-
for c in components
119-
]
120-
entity_version_pks = [
121-
(cv.pk if isinstance(cv, ComponentVersion) else None)
122-
for cv in components
123-
]
115+
if components is not None:
116+
for c in components:
117+
if not isinstance(c, (Component, ComponentVersion)):
118+
raise TypeError("Unit components must be either Component or ComponentVersion.")
119+
publishable_entities_pks = [
120+
(c.publishable_entity_id if isinstance(c, Component) else c.component.publishable_entity_id)
121+
for c in components
122+
]
123+
entity_version_pks = [
124+
(cv.pk if isinstance(cv, ComponentVersion) else None)
125+
for cv in components
126+
]
127+
else:
128+
# When these are None, that means don't change the entities in the list.
129+
publishable_entities_pks = None
130+
entity_version_pks = None
124131
with atomic():
125-
# TODO: how can we enforce that publishable entities must be components?
126-
# This currently allows for any publishable entity to be added to a unit.
127132
container_entity_version = container_api.create_next_container_version(
128133
unit.container_entity.pk,
129134
title=title,

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,26 @@ def test_query_count_of_contains_unpublished_changes(self):
554554
with self.assertNumQueries(2):
555555
assert authoring_api.contains_unpublished_changes(unit) is True
556556

557+
def test_metadata_change_doesnt_create_entity_list(self):
558+
"""
559+
Test that changing a container's metadata like title will create a new
560+
version, but can re-use the same EntityList. API consumers generally
561+
shouldn't depend on this behavior; it's an optimization.
562+
"""
563+
unit = self.create_unit_with_components([self.component_1, self.component_2_v1])
564+
565+
orig_version_num = unit.container_entity.versioning.draft.version_num
566+
orig_entity_list_id = unit.container_entity.versioning.draft.entity_list.pk
567+
568+
authoring_api.create_next_unit_version(unit, title="New Title", created=self.now)
569+
570+
unit.refresh_from_db()
571+
new_version_num = unit.container_entity.versioning.draft.version_num
572+
new_entity_list_id = unit.container_entity.versioning.draft.entity_list.pk
573+
574+
assert new_version_num > orig_version_num
575+
assert new_entity_list_id == orig_entity_list_id
576+
557577
@ddt.data(True, False)
558578
@pytest.mark.skip(reason="FIXME: we don't yet prevent adding soft-deleted components to units")
559579
def test_cannot_add_soft_deleted_component(self, publish_first):

0 commit comments

Comments
 (0)