openedx-learning 0.29.1__py2.py3-none-any.whl → 0.30.1__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/api.py +2 -2
- openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +8 -1
- openedx_learning/apps/authoring/backup_restore/zipper.py +17 -2
- 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.1.dist-info → openedx_learning-0.30.1.dist-info}/METADATA +5 -5
- {openedx_learning-0.29.1.dist-info → openedx_learning-0.30.1.dist-info}/RECORD +20 -18
- {openedx_learning-0.29.1.dist-info → openedx_learning-0.30.1.dist-info}/WHEEL +0 -0
- {openedx_learning-0.29.1.dist-info → openedx_learning-0.30.1.dist-info}/licenses/LICENSE.txt +0 -0
- {openedx_learning-0.29.1.dist-info → openedx_learning-0.30.1.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackag
|
|
|
9
9
|
from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def create_zip_file(lp_key: str, path: str, user: UserType | None = None) -> None:
|
|
12
|
+
def create_zip_file(lp_key: str, path: str, user: UserType | None = None, origin_server: str | None = None) -> None:
|
|
13
13
|
"""
|
|
14
14
|
Creates a dump zip file for the given learning package key at the given path.
|
|
15
15
|
The zip file contains a TOML representation of the learning package and its contents.
|
|
@@ -17,7 +17,7 @@ def create_zip_file(lp_key: str, path: str, user: UserType | None = None) -> Non
|
|
|
17
17
|
Can throw a NotFoundError at get_learning_package_by_key
|
|
18
18
|
"""
|
|
19
19
|
learning_package = get_learning_package_by_key(lp_key)
|
|
20
|
-
LearningPackageZipper(learning_package, user).create_zip(path)
|
|
20
|
+
LearningPackageZipper(learning_package, user, origin_server).create_zip(path)
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def load_learning_package(path: str, key: str | None = None, user: UserType | None = None) -> dict:
|
|
@@ -32,11 +32,18 @@ class Command(BaseCommand):
|
|
|
32
32
|
help='The username of the user performing the backup operation.',
|
|
33
33
|
default=None
|
|
34
34
|
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
'--origin_server',
|
|
37
|
+
type=str,
|
|
38
|
+
help='The origin server for the backup operation.',
|
|
39
|
+
default=None
|
|
40
|
+
)
|
|
35
41
|
|
|
36
42
|
def handle(self, *args, **options):
|
|
37
43
|
lp_key = options['lp_key']
|
|
38
44
|
file_name = options['file_name']
|
|
39
45
|
username = options['username']
|
|
46
|
+
origin_server = options['origin_server']
|
|
40
47
|
if not file_name.lower().endswith(".zip"):
|
|
41
48
|
raise CommandError("Output file name must end with .zip")
|
|
42
49
|
try:
|
|
@@ -45,7 +52,7 @@ class Command(BaseCommand):
|
|
|
45
52
|
if username:
|
|
46
53
|
user = User.objects.get(username=username)
|
|
47
54
|
start_time = time.time()
|
|
48
|
-
create_zip_file(lp_key, file_name, user=user)
|
|
55
|
+
create_zip_file(lp_key, file_name, user=user, origin_server=origin_server)
|
|
49
56
|
elapsed = time.time() - start_time
|
|
50
57
|
message = f'{lp_key} written to {file_name} (create_zip_file: {elapsed:.2f} seconds)'
|
|
51
58
|
self.stdout.write(self.style.SUCCESS(message))
|
|
@@ -88,9 +88,22 @@ class LearningPackageZipper:
|
|
|
88
88
|
A class to handle the zipping of learning content for backup and restore.
|
|
89
89
|
"""
|
|
90
90
|
|
|
91
|
-
def __init__(
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
learning_package: LearningPackage,
|
|
94
|
+
user: UserType | None = None,
|
|
95
|
+
origin_server: str | None = None):
|
|
96
|
+
"""
|
|
97
|
+
Initialize the LearningPackageZipper.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
learning_package (LearningPackage): The learning package to zip.
|
|
101
|
+
user (UserType | None): The user initiating the backup.
|
|
102
|
+
origin_server (str | None): The origin server for the backup.
|
|
103
|
+
"""
|
|
92
104
|
self.learning_package = learning_package
|
|
93
105
|
self.user = user
|
|
106
|
+
self.origin_server = origin_server
|
|
94
107
|
self.folders_already_created: set[Path] = set()
|
|
95
108
|
self.entities_filenames_already_created: set[str] = set()
|
|
96
109
|
self.utc_now = datetime.now(tz=timezone.utc)
|
|
@@ -269,7 +282,9 @@ class LearningPackageZipper:
|
|
|
269
282
|
|
|
270
283
|
with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
|
271
284
|
# Add the package.toml file
|
|
272
|
-
package_toml_content: str = toml_learning_package(
|
|
285
|
+
package_toml_content: str = toml_learning_package(
|
|
286
|
+
self.learning_package, self.utc_now, user=self.user, origin_server=self.origin_server
|
|
287
|
+
)
|
|
273
288
|
self.add_file_to_zip(zipf, Path(TOML_PACKAGE_NAME), package_toml_content, self.learning_package.updated)
|
|
274
289
|
|
|
275
290
|
# Add the entities directory
|
|
@@ -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
|
|