Skip to content

Commit 72d082c

Browse files
ormsbeekdmccormick
andauthored
feat: publishing dependencies and state tracking (#369)
This commit introduces the idea of dependencies to the publishing app. Dependencies are a generalization of unpinned container children that we plan to later use for things like course files & uploads, grading policies, and other things where a change in one publishable entity affects the state of another publishable entity, even if they do not share a direct parent/child relationship. Dependencies do not *replace* container children, but they offer a simpler model that allows the publishing app to quickly calculate things like, "Do any of the transitive dependencies of this entity have unpublished changes?". It also means that we can efficiently implement "publish this entity and all its dependencies at the same time" without having to use any callbacks to other apps that might implement different kinds of dependency relationships (e.g. grading policy). As long as those other apps declare their dependencies when calling create_publishable_entity_version(), the publishing app will have enough information to do these types of queries. It's worth noting that we never really wanted to put container logic directly in the publishing app, but we were forced to because the process of publishing is so coupled to parent/child relationships. This shift to thinking about dependencies rather than children will make it possible for us shift container logic out of publishing and into its own app. This commit also creates the PublishSideEffect as the publishing equivalent to DraftSideEffect. We've come to the tentative decision that we should probably try to unify some of these parallel draft/ published models, but that will be a bigger undertaking. This commit also introduces a dependencies_hash_digest field to DraftChangeLogRecord and PublishLogRecord in order to track the state of all live (current draft/published) dependencies. Tracking this has allowed us to optimize contains_unpublished_changes() to be a single query. Finally, this commit adds Django admin functionality for this new data. This admin interface is still rudimentary though, and could use a lot of work. --------- Co-authored-by: Kyle McCormick <[email protected]>
1 parent 0ac3994 commit 72d082c

File tree

18 files changed

+1490
-241
lines changed

18 files changed

+1490
-241
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.29.1"
5+
__version__ = "0.30.0"

openedx_learning/apps/authoring/publishing/admin.py

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import functools
77

88
from django.contrib import admin
9-
from django.db.models import Count
9+
from django.db.models import Count, F
1010
from django.utils.html import format_html
1111
from django.utils.safestring import SafeText
1212

@@ -21,6 +21,7 @@
2121
EntityListRow,
2222
LearningPackage,
2323
PublishableEntity,
24+
PublishableEntityVersion,
2425
PublishLog,
2526
PublishLogRecord,
2627
)
@@ -48,6 +49,7 @@ class PublishLogRecordTabularInline(admin.TabularInline):
4849
"title",
4950
"old_version_num",
5051
"new_version_num",
52+
"dependencies_hash_digest",
5153
)
5254
readonly_fields = fields
5355

@@ -89,28 +91,89 @@ class PublishLogAdmin(ReadOnlyModelAdmin):
8991
list_filter = ["learning_package"]
9092

9193

94+
class PublishableEntityVersionTabularInline(admin.TabularInline):
95+
"""
96+
Tabular inline for a single Draft change.
97+
"""
98+
model = PublishableEntityVersion
99+
100+
fields = (
101+
"version_num",
102+
"title",
103+
"created",
104+
"created_by",
105+
"dependencies_list",
106+
)
107+
readonly_fields = fields
108+
109+
def dependencies_list(self, version: PublishableEntityVersion):
110+
identifiers = sorted(
111+
[str(dep.key) for dep in version.dependencies.all()]
112+
)
113+
return "\n".join(identifiers)
114+
115+
def get_queryset(self, request):
116+
queryset = super().get_queryset(request)
117+
return (
118+
queryset
119+
.order_by('-version_num')
120+
.select_related('created_by', 'entity')
121+
.prefetch_related('dependencies')
122+
)
123+
124+
125+
class PublishStatusFilter(admin.SimpleListFilter):
126+
"""
127+
Custom filter for entities that have unpublished changes.
128+
"""
129+
title = "publish status"
130+
parameter_name = "publish_status"
131+
132+
def lookups(self, request, model_admin):
133+
return [
134+
("unpublished_changes", "Has unpublished changes"),
135+
]
136+
137+
def queryset(self, request, queryset):
138+
if self.value() == "unpublished_changes":
139+
return (
140+
queryset
141+
.exclude(
142+
published__version__isnull=True,
143+
draft__version__isnull=True,
144+
)
145+
.exclude(
146+
published__version=F("draft__version"),
147+
published__dependencies_hash_digest=F("draft__dependencies_hash_digest")
148+
)
149+
)
150+
return queryset
151+
152+
92153
@admin.register(PublishableEntity)
93154
class PublishableEntityAdmin(ReadOnlyModelAdmin):
94155
"""
95156
Read-only admin view for Publishable Entities
96157
"""
158+
inlines = [PublishableEntityVersionTabularInline]
159+
97160
list_display = [
98161
"key",
99-
"draft_version",
100162
"published_version",
163+
"draft_version",
101164
"uuid",
102165
"learning_package",
103166
"created",
104167
"created_by",
105168
"can_stand_alone",
106169
]
107-
list_filter = ["learning_package"]
170+
list_filter = ["learning_package", PublishStatusFilter]
108171
search_fields = ["key", "uuid"]
109172

110173
fields = [
111174
"key",
112-
"draft_version",
113175
"published_version",
176+
"draft_version",
114177
"uuid",
115178
"learning_package",
116179
"created",
@@ -120,8 +183,8 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
120183
]
121184
readonly_fields = [
122185
"key",
123-
"draft_version",
124186
"published_version",
187+
"draft_version",
125188
"uuid",
126189
"learning_package",
127190
"created",
@@ -130,21 +193,55 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
130193
"can_stand_alone",
131194
]
132195

133-
def draft_version(self, entity: PublishableEntity):
134-
return entity.draft.version.version_num if entity.draft.version else None
135-
136-
def published_version(self, entity: PublishableEntity):
137-
return entity.published.version.version_num if entity.published and entity.published.version else None
138-
139196
def get_queryset(self, request):
140197
queryset = super().get_queryset(request)
141198
return queryset.select_related(
142-
"learning_package", "published__version",
199+
"learning_package", "published__version", "draft__version", "created_by"
143200
)
144201

145202
def see_also(self, entity):
146203
return one_to_one_related_model_html(entity)
147204

205+
def draft_version(self, entity: PublishableEntity):
206+
"""
207+
Version num + dependency hash if applicable, e.g. "5" or "5 (825064c2)"
208+
209+
If the version info is different from the published version, we
210+
italicize the text for emphasis.
211+
"""
212+
if hasattr(entity, "draft") and entity.draft.version:
213+
draft_log_record = entity.draft.draft_log_record
214+
if draft_log_record and draft_log_record.dependencies_hash_digest:
215+
version_str = (
216+
f"{entity.draft.version.version_num} "
217+
f"({draft_log_record.dependencies_hash_digest})"
218+
)
219+
else:
220+
version_str = str(entity.draft.version.version_num)
221+
222+
if version_str == self.published_version(entity):
223+
return version_str
224+
else:
225+
return format_html("<em>{}</em>", version_str)
226+
227+
return None
228+
229+
def published_version(self, entity: PublishableEntity):
230+
"""
231+
Version num + dependency hash if applicable, e.g. "5" or "5 (825064c2)"
232+
"""
233+
if hasattr(entity, "published") and entity.published.version:
234+
publish_log_record = entity.published.publish_log_record
235+
if publish_log_record.dependencies_hash_digest:
236+
return (
237+
f"{entity.published.version.version_num} "
238+
f"({publish_log_record.dependencies_hash_digest})"
239+
)
240+
else:
241+
return str(entity.published.version.version_num)
242+
243+
return None
244+
148245

149246
@admin.register(Published)
150247
class PublishedAdmin(ReadOnlyModelAdmin):
@@ -197,6 +294,7 @@ class DraftChangeLogRecordTabularInline(admin.TabularInline):
197294
"title",
198295
"old_version_num",
199296
"new_version_num",
297+
"dependencies_hash_digest",
200298
)
201299
readonly_fields = fields
202300

0 commit comments

Comments
 (0)