Skip to content

Commit 4ee8f46

Browse files
Add a contains_unpublished_changes API
1 parent dd73ee5 commit 4ee8f46

File tree

2 files changed

+66
-10
lines changed
  • openedx_learning/apps/authoring/containers
  • tests/openedx_learning/apps/authoring/units

2 files changed

+66
-10
lines changed

openedx_learning/apps/authoring/containers/api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"ContainerEntityListEntry",
3535
"get_entities_in_draft_container",
3636
"get_entities_in_published_container",
37+
"contains_unpublished_changes",
3738
]
3839

3940

@@ -538,3 +539,45 @@ def get_entities_in_published_container(
538539
pinned=row.published_version is not None,
539540
))
540541
return entity_list
542+
543+
544+
def contains_unpublished_changes(
545+
container: ContainerEntity | ContainerEntityMixin,
546+
) -> bool:
547+
"""
548+
Check recursively if a container has any unpublished changes.
549+
550+
Note: container.versioning.has_unpublished_changes only checks if the container
551+
itself has unpublished changes, not if its contents do.
552+
"""
553+
if isinstance(container, ContainerEntityMixin):
554+
container = container.container_entity
555+
assert isinstance(container, ContainerEntity)
556+
557+
if container.versioning.has_unpublished_changes:
558+
return True
559+
560+
# We only care about children that are un-pinned, since published changes to pinned children don't matter
561+
defined_list = container.versioning.draft.defined_list
562+
563+
# TODO: This is a naive inefficient implementation but hopefully correct.
564+
# Once we know it's correct and have a good test suite, then we can optimize.
565+
# We will likely change to a tracking-based approach rather than a "scan for changes" based approach.
566+
for row in defined_list.entitylistrow_set.filter(draft_version=None):
567+
try:
568+
child_container = row.entity.containerentity
569+
except PublishableEntity.containerentity.RelatedObjectDoesNotExist:
570+
child_container = None
571+
if child_container:
572+
child_container = row.entity.containerentity
573+
# This is itself a container - check recursively:
574+
if child_container.versioning.has_unpublished_changes or contains_unpublished_changes(child_container):
575+
return True
576+
else:
577+
# This is not a container:
578+
draft_pk = row.entity.draft.version_id if row.entity.draft else None
579+
published_pk = row.entity.published.version_id if row.entity.published else None
580+
if draft_pk != published_pk:
581+
return True
582+
583+
return False

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ def test_add_component_after_publish(self):
111111
# Publish the empty unit:
112112
authoring_api.publish_all_drafts(self.learning_package.id)
113113
unit.refresh_from_db() # Reloading the unit is necessary
114-
assert unit.versioning.has_unpublished_changes == False
114+
assert unit.versioning.has_unpublished_changes == False # Shallow check for just the unit itself, not children
115+
assert authoring_api.contains_unpublished_changes(unit) == False # Deeper check
115116

116117
# Add a published component (unpinned):
117118
assert self.component_1.versioning.has_unpublished_changes == False
@@ -128,16 +129,17 @@ def test_add_component_after_publish(self):
128129
)
129130
# Now the unit should have unpublished changes:
130131
unit.refresh_from_db() # Reloading the unit is necessary
131-
assert unit.versioning.has_unpublished_changes == True
132+
assert unit.versioning.has_unpublished_changes == True # Shallow check - adding a child is a change to the unit
133+
assert authoring_api.contains_unpublished_changes(unit) == True # Deeper check
132134
assert unit.versioning.draft == unit_version_v2
133135
assert unit.versioning.published == unit_version
134136

135137
def test_modify_component_after_publish(self):
136138
"""
137139
Modifying a component in a published unit will NOT create a new version
138-
nor show that the unit has unpublished changes. The modifications will
139-
appear in the published version of the unit only after the component is
140-
published.
140+
nor show that the unit has unpublished changes (but it will "contain"
141+
unpublished changes). The modifications will appear in the published
142+
version of the unit only after the component is published.
141143
"""
142144
# Create a unit:
143145
unit, unit_version = authoring_api.create_unit_and_version(
@@ -164,7 +166,8 @@ def test_modify_component_after_publish(self):
164166
authoring_api.publish_all_drafts(self.learning_package.id)
165167
unit.refresh_from_db() # Reloading the unit is necessary
166168
self.component_1.refresh_from_db()
167-
assert unit.versioning.has_unpublished_changes == False
169+
assert unit.versioning.has_unpublished_changes == False # Shallow check
170+
assert authoring_api.contains_unpublished_changes(unit) == False # Deeper check
168171
assert self.component_1.versioning.has_unpublished_changes == False
169172

170173
# Now modify the component by changing its title (it remains a draft):
@@ -176,10 +179,11 @@ def test_modify_component_after_publish(self):
176179
created_by=None,
177180
)
178181

179-
# The component now has unpublished changes, but the unit doesn't (⭐️ Is this what we want? ⭐️)
182+
# The component now has unpublished changes; the unit doesn't directly but does contain
180183
unit.refresh_from_db() # Reloading the unit is necessary
181184
self.component_1.refresh_from_db()
182-
assert unit.versioning.has_unpublished_changes == False
185+
assert unit.versioning.has_unpublished_changes == False # Shallow check should be false - unit is unchanged
186+
assert authoring_api.contains_unpublished_changes(unit) == True # But unit DOES contain changes
183187
assert self.component_1.versioning.has_unpublished_changes == True
184188

185189
# Since the component changes haven't been published, they should only appear in the draft unit
@@ -198,13 +202,22 @@ def test_modify_component_after_publish(self):
198202
assert authoring_api.get_components_in_published_unit(unit) == [
199203
authoring_api.UnitListEntry(component_version=component_1_v2, pinned=False), # new version
200204
]
205+
assert authoring_api.contains_unpublished_changes(unit) == False # No longer contains unpublished changes
201206

202207

208+
# Test query count of contains_unpublished_changes()
203209
# Test that only components can be added to units
204210
# Test that components must be in the same learning package
205211
# Test that _version_pks=[] arguments must be related to publishable_entities_pks
206-
# Test that publishing a unit publishes its components
207-
# Test viewing old snapshots of units and components by passing in a timestamp to some get_historic_unit() API
212+
# Test that publishing a unit publishes its child components (either automatically or throws an exception if you
213+
# don't request the children be published together with the containeer)
214+
# Test that publishing a component does NOT publish changes to its parent unit
215+
# Test that I can get a history of a given unit and all its children, including children that aren't currently in
216+
# the unit and excluding children that are only in other units.
217+
# Test that I can get a history of a given unit and its children, that includes changes made to the child components
218+
# while they were part of the unit but excludes changes made to those children while they were not part of
219+
# the unit. 🫣
220+
# Test viewing old snapshots of units and components by passing in a timestamp to some get_historic_unit() API?
208221

209222

210223
def test_next_version_with_different_different_title(self):

0 commit comments

Comments
 (0)