Skip to content

Commit c17a738

Browse files
feat: API to get a historic published version of a unit
1 parent fd83bd6 commit c17a738

File tree

4 files changed

+105
-10
lines changed

4 files changed

+105
-10
lines changed

openedx_learning/apps/authoring/containers/api.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,10 @@ def create_next_defined_list(
108108
# 2. Only associate existent rows to the new entity list iff: the order is the same, the PublishableEntity is in entity_pks and versions are not pinned
109109
# 3. If the order is different for a row with the PublishableEntity, create new row with the same PublishableEntity for the new order
110110
# and associate the new row to the new entity list
111-
current_rows = previous_entity_list.entitylistrow_set.all()
112-
publishable_entities_in_rows = {row.entity.pk: row for row in current_rows}
113111
new_rows = []
114112
for order_num, entity_pk, entity_version_pk in zip(
115113
order_nums, entity_pks, entity_version_pks
116114
):
117-
row = publishable_entities_in_rows.get(entity_pk)
118-
if row and row.order_num == order_num:
119-
new_entity_list.entitylistrow_set.add(row)
120-
continue
121115
new_rows.append(
122116
EntityListRow(
123117
entity_list=new_entity_list,

openedx_learning/apps/authoring/publishing/api.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,18 @@ def filter_publishable_entities(
513513
entities = entities.filter(published__version__isnull=not has_published)
514514

515515
return entities
516+
517+
518+
def get_published_version_as_of(entity_id: int, publish_log_id: int) -> PublishableEntityVersion | None:
519+
"""
520+
Get the published version of the given entity, at a specific snapshot in the
521+
history of this Learning Package, given by the PublishLog ID.
522+
523+
This is a semi-private function, only available to other apps in the
524+
authoring package.
525+
"""
526+
record = PublishLogRecord.objects.filter(
527+
entity_id=entity_id,
528+
publish_log_id__lte=publish_log_id,
529+
).order_by('-publish_log_id').first()
530+
return record.new_version if record else None

openedx_learning/apps/authoring/units/api.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from openedx_learning.apps.authoring.components.models import Component, ComponentVersion
1010
from openedx_learning.apps.authoring.containers.models import EntityListRow
1111
from ..publishing import api as publishing_api
12+
from ..publishing.api import get_published_version_as_of
1213
from ..containers import api as container_api
1314
from .models import Unit, UnitVersion
1415
from django.db.models import QuerySet
@@ -29,6 +30,7 @@
2930
"UnitListEntry",
3031
"get_components_in_draft_unit",
3132
"get_components_in_published_unit",
33+
"get_components_in_published_unit_as_of",
3234
]
3335

3436

@@ -240,7 +242,7 @@ def get_components_in_published_unit(
240242
) -> list[UnitListEntry]:
241243
"""
242244
[ 🛑 UNSTABLE ]
243-
Get the list of entities and their versions in the draft version of the
245+
Get the list of entities and their versions in the published version of the
244246
given container.
245247
"""
246248
assert isinstance(unit, Unit)
@@ -254,3 +256,35 @@ def get_components_in_published_unit(
254256
assert isinstance(component_version, ComponentVersion)
255257
entity_list.append(UnitListEntry(component_version=component_version, pinned=entry.pinned))
256258
return entity_list
259+
260+
261+
def get_components_in_published_unit_as_of(
262+
unit: Unit,
263+
publish_log_id: int, # TODO: or UUID
264+
) -> list[UnitListEntry] | None:
265+
"""
266+
[ 🛑 UNSTABLE ]
267+
Get the list of entities and their versions in the published version of the
268+
given container as of the given PublishLog version (which is essentially a
269+
version for the entire learning package).
270+
"""
271+
assert isinstance(unit, Unit)
272+
unit_pub_entity_version = get_published_version_as_of(unit.publishable_entity, publish_log_id)
273+
if unit_pub_entity_version is None:
274+
return None # This unit was not published as of the given PublishLog ID.
275+
unit_version = unit_pub_entity_version.unitversion
276+
277+
entity_list = []
278+
rows = unit_version.container_entity_version.defined_list.entitylistrow_set.order_by("order_num")
279+
for row in rows:
280+
if row.entity_version is not None:
281+
component_version = row.entity_version.componentversion
282+
assert isinstance(component_version, ComponentVersion)
283+
entity_list.append(UnitListEntry(component_version=component_version, pinned=True))
284+
else:
285+
# Unpinned component - figure out what its latest published version was.
286+
# This is not optimized. It could be done in one query per unit rather than one query per component.
287+
pub_entity_version = get_published_version_as_of(row.entity, publish_log_id)
288+
if pub_entity_version:
289+
entity_list.append(UnitListEntry(component_version=pub_entity_version.componentversion, pinned=False))
290+
return entity_list

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

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def test_create_next_unit_version_with_two_unpinned_components(self):
133133

134134
def test_create_next_unit_version_with_unpinned_and_pinned_components(self):
135135
"""
136-
Test creating a unit version with one unpinned and one pinned component.
136+
Test creating a unit version with one unpinned and one pinned 📌 component.
137137
"""
138138
unit, unit_version = authoring_api.create_unit_and_version(
139139
learning_package_id=self.learning_package.id,
@@ -145,15 +145,15 @@ def test_create_next_unit_version_with_unpinned_and_pinned_components(self):
145145
unit_version_v2 = authoring_api.create_next_unit_version(
146146
unit=unit,
147147
title="Unit",
148-
components=[self.component_1, self.component_2_v1], # Note the "v1" pinning it to version 2
148+
components=[self.component_1, self.component_2_v1], # Note the "v1" pinning 📌 the second one to version 1
149149
created=self.now,
150150
created_by=None,
151151
)
152152
assert unit_version_v2.version_num == 2
153153
assert unit_version_v2 in unit.versioning.versions.all()
154154
assert authoring_api.get_components_in_draft_unit(unit) == [
155155
authoring_api.UnitListEntry(component_version=self.component_1_v1, pinned=False),
156-
authoring_api.UnitListEntry(component_version=self.component_2_v1, pinned=True), # Pinned to v1
156+
authoring_api.UnitListEntry(component_version=self.component_2_v1, pinned=True), # Pinned 📌 to v1
157157
]
158158
assert authoring_api.get_components_in_published_unit(unit) is None
159159

@@ -318,6 +318,58 @@ def test_query_count_of_contains_unpublished_changes(self):
318318
# Test viewing old snapshots of units and components by passing in a timestamp (or PublishLog PK) to a
319319
# get_historic_unit() API?
320320

321+
def test_snapshots_of_published_unit(self):
322+
"""
323+
Test that we can access snapshots of the historic published version of
324+
units and their contents.
325+
"""
326+
# At first the unit has one component (unpinned):
327+
unit = self.create_unit_with_components([self.component_1])
328+
self.modify_component(self.component_1, title="Component 1 as of checkpoint 1")
329+
330+
# Publish everything, creating Checkpoint 1
331+
checkpoint_1 = authoring_api.publish_all_drafts(self.learning_package.id, message="checkpoint 1")
332+
333+
########################################################################
334+
335+
# Now we update the title of the component.
336+
self.modify_component(self.component_1, title="Component 1 as of checkpoint 2")
337+
# Publish everything, creating Checkpoint 2
338+
checkpoint_2 = authoring_api.publish_all_drafts(self.learning_package.id, message="checkpoint 2")
339+
########################################################################
340+
341+
# Now add a second component to the unit:
342+
self.modify_component(self.component_1, title="Component 1 as of checkpoint 3")
343+
self.modify_component(self.component_2, title="Component 2 as of checkpoint 3")
344+
authoring_api.create_next_unit_version(
345+
unit=unit,
346+
title="Unit title in checkpoint 3",
347+
components=[self.component_1, self.component_2],
348+
created=self.now,
349+
)
350+
# Publish everything, creating Checkpoint 3
351+
checkpoint_3 = authoring_api.publish_all_drafts(self.learning_package.id, message="checkpoint 3")
352+
########################################################################
353+
354+
# Modify the drafts, but don't publish:
355+
self.modify_component(self.component_1, title="Component 1 draft")
356+
self.modify_component(self.component_2, title="Component 2 draft")
357+
358+
# Now fetch the snapshots:
359+
as_of_checkpoint_1 = authoring_api.get_components_in_published_unit_as_of(unit, checkpoint_1.pk)
360+
assert [cv.component_version.title for cv in as_of_checkpoint_1] == [
361+
"Component 1 as of checkpoint 1",
362+
]
363+
as_of_checkpoint_2 = authoring_api.get_components_in_published_unit_as_of(unit, checkpoint_2.pk)
364+
assert [cv.component_version.title for cv in as_of_checkpoint_2] == [
365+
"Component 1 as of checkpoint 2",
366+
]
367+
as_of_checkpoint_3 = authoring_api.get_components_in_published_unit_as_of(unit, checkpoint_3.pk)
368+
assert [cv.component_version.title for cv in as_of_checkpoint_3] == [
369+
"Component 1 as of checkpoint 3",
370+
"Component 2 as of checkpoint 3",
371+
]
372+
321373
def test_next_version_with_different_different_title(self):
322374
"""Test creating a unit version with a different title.
323375

0 commit comments

Comments
 (0)