openedx-learning 0.29.0__py2.py3-none-any.whl → 0.30.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_learning/__init__.py +1 -1
- openedx_learning/apps/authoring/backup_restore/zipper.py +22 -4
- openedx_learning/apps/authoring/publishing/admin.py +110 -12
- openedx_learning/apps/authoring/publishing/api.py +676 -200
- openedx_learning/apps/authoring/publishing/migrations/0009_dependencies_and_hashing.py +62 -0
- openedx_learning/apps/authoring/publishing/migrations/0010_backfill_dependencies.py +149 -0
- openedx_learning/apps/authoring/publishing/models/__init__.py +2 -1
- openedx_learning/apps/authoring/publishing/models/draft_log.py +77 -1
- openedx_learning/apps/authoring/publishing/models/entity_list.py +19 -0
- openedx_learning/apps/authoring/publishing/models/publish_log.py +87 -1
- openedx_learning/apps/authoring/publishing/models/publishable_entity.py +48 -0
- openedx_learning/apps/authoring/units/api.py +1 -1
- openedx_learning/lib/fields.py +13 -11
- {openedx_learning-0.29.0.dist-info → openedx_learning-0.30.0.dist-info}/METADATA +5 -5
- {openedx_learning-0.29.0.dist-info → openedx_learning-0.30.0.dist-info}/RECORD +18 -16
- {openedx_learning-0.29.0.dist-info → openedx_learning-0.30.0.dist-info}/WHEEL +0 -0
- {openedx_learning-0.29.0.dist-info → openedx_learning-0.30.0.dist-info}/licenses/LICENSE.txt +0 -0
- {openedx_learning-0.29.0.dist-info → openedx_learning-0.30.0.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -47,6 +47,7 @@ from openedx_learning.apps.authoring.backup_restore.toml import (
|
|
|
47
47
|
)
|
|
48
48
|
from openedx_learning.apps.authoring.collections import api as collections_api
|
|
49
49
|
from openedx_learning.apps.authoring.components import api as components_api
|
|
50
|
+
from openedx_learning.apps.authoring.contents import api as contents_api
|
|
50
51
|
from openedx_learning.apps.authoring.publishing import api as publishing_api
|
|
51
52
|
from openedx_learning.apps.authoring.sections import api as sections_api
|
|
52
53
|
from openedx_learning.apps.authoring.subsections import api as subsections_api
|
|
@@ -493,6 +494,7 @@ class LearningPackageUnzipper:
|
|
|
493
494
|
self.zipf = zipf
|
|
494
495
|
self.user = user
|
|
495
496
|
self.lp_key = key # If provided, use this key for the restored learning package
|
|
497
|
+
self.learning_package_id: int | None = None # Will be set upon restoration
|
|
496
498
|
self.utc_now: datetime = datetime.now(timezone.utc)
|
|
497
499
|
self.component_types_cache: dict[tuple[str, str], ComponentType] = {}
|
|
498
500
|
self.errors: list[dict[str, Any]] = []
|
|
@@ -735,6 +737,7 @@ class LearningPackageUnzipper:
|
|
|
735
737
|
learning_package["key"] = self.lp_key
|
|
736
738
|
|
|
737
739
|
learning_package_obj = publishing_api.create_learning_package(**learning_package)
|
|
740
|
+
self.learning_package_id = learning_package_obj.id
|
|
738
741
|
|
|
739
742
|
with publishing_api.bulk_draft_changes_for(learning_package_obj.id):
|
|
740
743
|
self._save_components(learning_package_obj, components, component_static_files)
|
|
@@ -937,16 +940,31 @@ class LearningPackageUnzipper:
|
|
|
937
940
|
num_version: int,
|
|
938
941
|
entity_key: str,
|
|
939
942
|
static_files_map: dict[str, List[str]]
|
|
940
|
-
) -> dict[str, bytes]:
|
|
943
|
+
) -> dict[str, bytes | int]:
|
|
941
944
|
"""Resolve static file paths into their binary content."""
|
|
942
|
-
resolved_files: dict[str, bytes] = {}
|
|
945
|
+
resolved_files: dict[str, bytes | int] = {}
|
|
943
946
|
|
|
944
|
-
static_file_key = f"{entity_key}:v{num_version}" # e.g., "
|
|
947
|
+
static_file_key = f"{entity_key}:v{num_version}" # e.g., "xblock.v1:html:my_component_123456:v1"
|
|
948
|
+
block_type = entity_key.split(":")[1] # e.g., "html"
|
|
945
949
|
static_files = static_files_map.get(static_file_key, [])
|
|
946
950
|
for static_file in static_files:
|
|
947
951
|
local_key = static_file.split(f"v{num_version}/")[-1]
|
|
948
952
|
with self.zipf.open(static_file, "r") as f:
|
|
949
|
-
|
|
953
|
+
content_bytes = f.read()
|
|
954
|
+
if local_key == "block.xml":
|
|
955
|
+
# Special handling for block.xml to ensure
|
|
956
|
+
# storing the value as a content instance
|
|
957
|
+
if not self.learning_package_id:
|
|
958
|
+
raise ValueError("learning_package_id must be set before resolving static files.")
|
|
959
|
+
text_content = contents_api.get_or_create_text_content(
|
|
960
|
+
self.learning_package_id,
|
|
961
|
+
contents_api.get_or_create_media_type(f"application/vnd.openedx.xblock.v1.{block_type}+xml").id,
|
|
962
|
+
text=content_bytes.decode("utf-8"),
|
|
963
|
+
created=self.utc_now,
|
|
964
|
+
)
|
|
965
|
+
resolved_files[local_key] = text_content.id
|
|
966
|
+
else:
|
|
967
|
+
resolved_files[local_key] = content_bytes
|
|
950
968
|
return resolved_files
|
|
951
969
|
|
|
952
970
|
def _resolve_children(self, entity_data: dict[str, Any], lookup_map: dict[str, Any]) -> list[Any]:
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import functools
|
|
7
7
|
|
|
8
8
|
from django.contrib import admin
|
|
9
|
-
from django.db.models import Count
|
|
9
|
+
from django.db.models import Count, F
|
|
10
10
|
from django.utils.html import format_html
|
|
11
11
|
from django.utils.safestring import SafeText
|
|
12
12
|
|
|
@@ -21,6 +21,7 @@ from .models import (
|
|
|
21
21
|
EntityListRow,
|
|
22
22
|
LearningPackage,
|
|
23
23
|
PublishableEntity,
|
|
24
|
+
PublishableEntityVersion,
|
|
24
25
|
PublishLog,
|
|
25
26
|
PublishLogRecord,
|
|
26
27
|
)
|
|
@@ -48,6 +49,7 @@ class PublishLogRecordTabularInline(admin.TabularInline):
|
|
|
48
49
|
"title",
|
|
49
50
|
"old_version_num",
|
|
50
51
|
"new_version_num",
|
|
52
|
+
"dependencies_hash_digest",
|
|
51
53
|
)
|
|
52
54
|
readonly_fields = fields
|
|
53
55
|
|
|
@@ -89,28 +91,89 @@ class PublishLogAdmin(ReadOnlyModelAdmin):
|
|
|
89
91
|
list_filter = ["learning_package"]
|
|
90
92
|
|
|
91
93
|
|
|
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
|
+
|
|
92
153
|
@admin.register(PublishableEntity)
|
|
93
154
|
class PublishableEntityAdmin(ReadOnlyModelAdmin):
|
|
94
155
|
"""
|
|
95
156
|
Read-only admin view for Publishable Entities
|
|
96
157
|
"""
|
|
158
|
+
inlines = [PublishableEntityVersionTabularInline]
|
|
159
|
+
|
|
97
160
|
list_display = [
|
|
98
161
|
"key",
|
|
99
|
-
"draft_version",
|
|
100
162
|
"published_version",
|
|
163
|
+
"draft_version",
|
|
101
164
|
"uuid",
|
|
102
165
|
"learning_package",
|
|
103
166
|
"created",
|
|
104
167
|
"created_by",
|
|
105
168
|
"can_stand_alone",
|
|
106
169
|
]
|
|
107
|
-
list_filter = ["learning_package"]
|
|
170
|
+
list_filter = ["learning_package", PublishStatusFilter]
|
|
108
171
|
search_fields = ["key", "uuid"]
|
|
109
172
|
|
|
110
173
|
fields = [
|
|
111
174
|
"key",
|
|
112
|
-
"draft_version",
|
|
113
175
|
"published_version",
|
|
176
|
+
"draft_version",
|
|
114
177
|
"uuid",
|
|
115
178
|
"learning_package",
|
|
116
179
|
"created",
|
|
@@ -120,8 +183,8 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
|
|
|
120
183
|
]
|
|
121
184
|
readonly_fields = [
|
|
122
185
|
"key",
|
|
123
|
-
"draft_version",
|
|
124
186
|
"published_version",
|
|
187
|
+
"draft_version",
|
|
125
188
|
"uuid",
|
|
126
189
|
"learning_package",
|
|
127
190
|
"created",
|
|
@@ -130,21 +193,55 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
|
|
|
130
193
|
"can_stand_alone",
|
|
131
194
|
]
|
|
132
195
|
|
|
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
|
-
|
|
139
196
|
def get_queryset(self, request):
|
|
140
197
|
queryset = super().get_queryset(request)
|
|
141
198
|
return queryset.select_related(
|
|
142
|
-
"learning_package", "published__version",
|
|
199
|
+
"learning_package", "published__version", "draft__version", "created_by"
|
|
143
200
|
)
|
|
144
201
|
|
|
145
202
|
def see_also(self, entity):
|
|
146
203
|
return one_to_one_related_model_html(entity)
|
|
147
204
|
|
|
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
|
+
|
|
148
245
|
|
|
149
246
|
@admin.register(Published)
|
|
150
247
|
class PublishedAdmin(ReadOnlyModelAdmin):
|
|
@@ -197,6 +294,7 @@ class DraftChangeLogRecordTabularInline(admin.TabularInline):
|
|
|
197
294
|
"title",
|
|
198
295
|
"old_version_num",
|
|
199
296
|
"new_version_num",
|
|
297
|
+
"dependencies_hash_digest",
|
|
200
298
|
)
|
|
201
299
|
readonly_fields = fields
|
|
202
300
|
|