openedx-learning 0.26.0__py2.py3-none-any.whl → 0.27.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.
Files changed (34) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/api/authoring.py +1 -0
  3. openedx_learning/apps/authoring/backup_restore/__init__.py +0 -0
  4. openedx_learning/apps/authoring/backup_restore/admin.py +3 -0
  5. openedx_learning/apps/authoring/backup_restore/api.py +15 -0
  6. openedx_learning/apps/authoring/backup_restore/apps.py +12 -0
  7. openedx_learning/apps/authoring/backup_restore/management/__init__.py +0 -0
  8. openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py +0 -0
  9. openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +44 -0
  10. openedx_learning/apps/authoring/backup_restore/migrations/__init__.py +0 -0
  11. openedx_learning/apps/authoring/backup_restore/models.py +3 -0
  12. openedx_learning/apps/authoring/backup_restore/toml.py +75 -0
  13. openedx_learning/apps/authoring/backup_restore/zipper.py +53 -0
  14. openedx_learning/apps/authoring/components/apps.py +2 -2
  15. openedx_learning/apps/authoring/publishing/admin.py +277 -12
  16. openedx_learning/apps/authoring/publishing/api.py +41 -8
  17. openedx_learning/apps/authoring/publishing/apps.py +2 -2
  18. openedx_learning/apps/authoring/publishing/models/publishable_entity.py +20 -3
  19. openedx_learning/apps/authoring/sections/admin.py +48 -0
  20. openedx_learning/apps/authoring/sections/api.py +6 -2
  21. openedx_learning/apps/authoring/sections/apps.py +4 -4
  22. openedx_learning/apps/authoring/subsections/admin.py +48 -0
  23. openedx_learning/apps/authoring/subsections/api.py +6 -2
  24. openedx_learning/apps/authoring/subsections/apps.py +2 -2
  25. openedx_learning/apps/authoring/units/admin.py +48 -0
  26. openedx_learning/apps/authoring/units/api.py +6 -2
  27. openedx_learning/apps/authoring/units/apps.py +2 -2
  28. openedx_learning/lib/admin_utils.py +17 -1
  29. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/METADATA +4 -3
  30. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/RECORD +34 -20
  31. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/WHEEL +1 -1
  32. openedx_tagging/core/tagging/models/base.py +1 -1
  33. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/licenses/LICENSE.txt +0 -0
  34. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/top_level.txt +0 -0
@@ -2,4 +2,4 @@
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
4
 
5
- __version__ = "0.26.0"
5
+ __version__ = "0.27.1"
@@ -9,6 +9,7 @@ APIs.
9
9
  """
10
10
  # These wildcard imports are okay because these api modules declare __all__.
11
11
  # pylint: disable=wildcard-import
12
+ from ..apps.authoring.backup_restore.api import *
12
13
  from ..apps.authoring.collections.api import *
13
14
  from ..apps.authoring.components.api import *
14
15
  from ..apps.authoring.contents.api import *
@@ -0,0 +1,3 @@
1
+ """
2
+ Django Admin pages for Backup Restore models (WIP)
3
+ """
@@ -0,0 +1,15 @@
1
+ """
2
+ Backup Restore API
3
+ """
4
+ from openedx_learning.apps.authoring.backup_restore.zipper import LearningPackageZipper
5
+ from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key
6
+
7
+
8
+ def create_zip_file(lp_key: str, path: str) -> None:
9
+ """
10
+ Creates a zip file with a toml file so far (WIP)
11
+
12
+ Can throw a NotFoundError at get_learning_package_by_key
13
+ """
14
+ learning_package = get_learning_package_by_key(lp_key)
15
+ LearningPackageZipper(learning_package).create_zip(path)
@@ -0,0 +1,12 @@
1
+ """
2
+ Backup/Restore application initialization.
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+
7
+
8
+ class BackupRestoreConfig(AppConfig):
9
+ name = 'openedx_learning.apps.authoring.backup_restore'
10
+ verbose_name = "Learning Core > Authoring > Backup Restore"
11
+ default_auto_field = 'django.db.models.BigAutoField'
12
+ label = "oel_backup_restore"
@@ -0,0 +1,44 @@
1
+ """
2
+ Django management commands to handle backup and restore learning packages (WIP)
3
+ """
4
+ import logging
5
+
6
+ from django.core.management import CommandError
7
+ from django.core.management.base import BaseCommand
8
+
9
+ from openedx_learning.apps.authoring.backup_restore.api import create_zip_file
10
+ from openedx_learning.apps.authoring.publishing.api import LearningPackage
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Command(BaseCommand):
16
+ """
17
+ Django management command to export a learning package to a zip file.
18
+ """
19
+ help = 'Export a learning package to a zip file.'
20
+
21
+ def add_arguments(self, parser):
22
+ parser.add_argument('lp_key', type=str, help='The key of the LearningPackage to dump')
23
+ parser.add_argument('file_name', type=str, help='The name of the output zip file')
24
+
25
+ def handle(self, *args, **options):
26
+ lp_key = options['lp_key']
27
+ file_name = options['file_name']
28
+ if not file_name.endswith(".zip"):
29
+ raise CommandError("Output file name must end with .zip")
30
+ try:
31
+ create_zip_file(lp_key, file_name)
32
+ message = f'{lp_key} written to {file_name}'
33
+ self.stdout.write(self.style.SUCCESS(message))
34
+ except LearningPackage.DoesNotExist as exc:
35
+ message = f"Learning package with key {lp_key} not found"
36
+ raise CommandError(message) from exc
37
+ except Exception as e:
38
+ message = f"Failed to export learning package '{lp_key}': {e}"
39
+ logger.exception(
40
+ "Failed to create zip file %s (learning‑package key %s)",
41
+ file_name,
42
+ lp_key,
43
+ )
44
+ raise CommandError(message) from e
@@ -0,0 +1,3 @@
1
+ """
2
+ Core models for Backup Restore (WIP)
3
+ """
@@ -0,0 +1,75 @@
1
+ """
2
+ TOML serialization for learning packages and publishable entities.
3
+ """
4
+
5
+ from datetime import datetime
6
+
7
+ import tomlkit
8
+
9
+ from openedx_learning.apps.authoring.publishing.models import PublishableEntity, PublishableEntityVersion
10
+ from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
11
+
12
+
13
+ def toml_learning_package(learning_package: LearningPackage) -> str:
14
+ """Create a TOML representation of the learning package."""
15
+ doc = tomlkit.document()
16
+ doc.add(tomlkit.comment(f"Datetime of the export: {datetime.now()}"))
17
+ section = tomlkit.table()
18
+ section.add("title", learning_package.title)
19
+ section.add("key", learning_package.key)
20
+ section.add("description", learning_package.description)
21
+ section.add("created", learning_package.created)
22
+ section.add("updated", learning_package.updated)
23
+ doc.add("learning_package", section)
24
+ return tomlkit.dumps(doc)
25
+
26
+
27
+ def toml_publishable_entity(entity: PublishableEntity) -> str:
28
+ """Create a TOML representation of a publishable entity."""
29
+
30
+ current_draft_version = getattr(entity, "draft", None)
31
+ current_published_version = getattr(entity, "published", None)
32
+
33
+ doc = tomlkit.document()
34
+ entity_table = tomlkit.table()
35
+ entity_table.add("uuid", str(entity.uuid))
36
+ entity_table.add("can_stand_alone", entity.can_stand_alone)
37
+
38
+ if current_draft_version:
39
+ draft_table = tomlkit.table()
40
+ draft_table.add("version_num", current_draft_version.version.version_num)
41
+ entity_table.add("draft", draft_table)
42
+
43
+ published_table = tomlkit.table()
44
+ if current_published_version:
45
+ published_table.add("version_num", current_published_version.version.version_num)
46
+ else:
47
+ published_table.add(tomlkit.comment("unpublished: no published_version_num"))
48
+ entity_table.add("published", published_table)
49
+
50
+ doc.add("entity", entity_table)
51
+ doc.add(tomlkit.nl())
52
+ doc.add(tomlkit.comment("### Versions"))
53
+
54
+ for entity_version in entity.versions.all():
55
+ version = tomlkit.aot()
56
+ version_table = toml_publishable_entity_version(entity_version)
57
+ version.append(version_table)
58
+ doc.add("version", version)
59
+
60
+ return tomlkit.dumps(doc)
61
+
62
+
63
+ def toml_publishable_entity_version(version: PublishableEntityVersion) -> tomlkit.items.Table:
64
+ """Create a TOML representation of a publishable entity version."""
65
+ version_table = tomlkit.table()
66
+ version_table.add("title", version.title)
67
+ version_table.add("uuid", str(version.uuid))
68
+ version_table.add("version_num", version.version_num)
69
+ container_table = tomlkit.table()
70
+ container_table.add("children", [])
71
+ unit_table = tomlkit.table()
72
+ unit_table.add("graded", True)
73
+ container_table.add("unit", unit_table)
74
+ version_table.add("container", container_table)
75
+ return version_table # For use in AoT
@@ -0,0 +1,53 @@
1
+ """
2
+ This module provides functionality to create a zip file containing the learning package data,
3
+ including a TOML representation of the learning package and its entities.
4
+ """
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ from openedx_learning.apps.authoring.backup_restore.toml import toml_learning_package, toml_publishable_entity
9
+ from openedx_learning.apps.authoring.publishing import api as publishing_api
10
+ from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
11
+
12
+ TOML_PACKAGE_NAME = "package.toml"
13
+
14
+
15
+ class LearningPackageZipper:
16
+ """
17
+ A class to handle the zipping of learning content for backup and restore.
18
+ """
19
+
20
+ def __init__(self, learning_package: LearningPackage):
21
+ self.learning_package = learning_package
22
+
23
+ def create_zip(self, path: str) -> None:
24
+ """
25
+ Creates a zip file containing the learning package data.
26
+ Args:
27
+ path (str): The path where the zip file will be created.
28
+ Raises:
29
+ Exception: If the learning package cannot be found or if the zip creation fails.
30
+ """
31
+ package_toml_content: str = toml_learning_package(self.learning_package)
32
+
33
+ with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
34
+ # Add the package.toml string
35
+ zipf.writestr(TOML_PACKAGE_NAME, package_toml_content)
36
+
37
+ # Add the entities directory
38
+ entities_folder = Path("entities")
39
+ zip_info = zipfile.ZipInfo(str(entities_folder) + "/") # Ensure trailing slash
40
+ zipf.writestr(zip_info, "") # Add explicit empty directory entry
41
+
42
+ # Add the collections directory
43
+ collections_folder = Path("collections")
44
+ collections_info = zipfile.ZipInfo(str(collections_folder) + "/") # Ensure trailing slash
45
+ zipf.writestr(collections_info, "") # Add explicit empty directory
46
+
47
+ # Add each entity's TOML file
48
+ for entity in publishing_api.get_entities(self.learning_package.pk):
49
+ # Create a TOML representation of the entity
50
+ entity_toml_content: str = toml_publishable_entity(entity)
51
+ entity_toml_filename = f"{entity.key}.toml"
52
+ entity_toml_path = entities_folder / entity_toml_filename
53
+ zipf.writestr(str(entity_toml_path), entity_toml_content)
@@ -18,7 +18,7 @@ class ComponentsConfig(AppConfig):
18
18
  """
19
19
  Register Component and ComponentVersion.
20
20
  """
21
- from ..publishing.api import register_content_models # pylint: disable=import-outside-toplevel
21
+ from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
22
22
  from .models import Component, ComponentVersion # pylint: disable=import-outside-toplevel
23
23
 
24
- register_content_models(Component, ComponentVersion)
24
+ register_publishable_models(Component, ComponentVersion)
@@ -3,14 +3,22 @@ Django admin for publishing models
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
+ import functools
7
+
6
8
  from django.contrib import admin
7
9
  from django.db.models import Count
10
+ from django.utils.html import format_html
11
+ from django.utils.safestring import SafeText
8
12
 
9
- from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, one_to_one_related_model_html
13
+ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, model_detail_link, one_to_one_related_model_html
10
14
 
11
15
  from .models import (
16
+ Container,
17
+ ContainerVersion,
12
18
  DraftChangeLog,
13
19
  DraftChangeLogRecord,
20
+ EntityList,
21
+ EntityListRow,
14
22
  LearningPackage,
15
23
  PublishableEntity,
16
24
  PublishLog,
@@ -122,6 +130,12 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
122
130
  "can_stand_alone",
123
131
  ]
124
132
 
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
+
125
139
  def get_queryset(self, request):
126
140
  queryset = super().get_queryset(request)
127
141
  return queryset.select_related(
@@ -131,16 +145,6 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
131
145
  def see_also(self, entity):
132
146
  return one_to_one_related_model_html(entity)
133
147
 
134
- def draft_version(self, entity):
135
- if entity.draft.version:
136
- return entity.draft.version.version_num
137
- return None
138
-
139
- def published_version(self, entity):
140
- if entity.published.version:
141
- return entity.published.version.version_num
142
- return None
143
-
144
148
 
145
149
  @admin.register(Published)
146
150
  class PublishedAdmin(ReadOnlyModelAdmin):
@@ -229,7 +233,7 @@ class DraftChangeSetAdmin(ReadOnlyModelAdmin):
229
233
  """
230
234
  inlines = [DraftChangeLogRecordTabularInline]
231
235
  fields = (
232
- "uuid",
236
+ "pk",
233
237
  "learning_package",
234
238
  "num_changes",
235
239
  "changed_at",
@@ -246,3 +250,264 @@ class DraftChangeSetAdmin(ReadOnlyModelAdmin):
246
250
  queryset = super().get_queryset(request)
247
251
  return queryset.select_related("learning_package", "changed_by") \
248
252
  .annotate(num_changes=Count("records"))
253
+
254
+
255
+ def _entity_list_detail_link(el: EntityList) -> SafeText:
256
+ """
257
+ A link to the detail page for an EntityList which includes its PK and length.
258
+ """
259
+ num_rows = el.entitylistrow_set.count()
260
+ rows_noun = "row" if num_rows == 1 else "rows"
261
+ return model_detail_link(el, f"EntityList #{el.pk} with {num_rows} {rows_noun}")
262
+
263
+
264
+ class ContainerVersionInlineForContainer(admin.TabularInline):
265
+ """
266
+ Inline admin view of ContainerVersions in a given Container
267
+ """
268
+ model = ContainerVersion
269
+ ordering = ["-publishable_entity_version__version_num"]
270
+ fields = [
271
+ "pk",
272
+ "version_num",
273
+ "title",
274
+ "children",
275
+ "created",
276
+ "created_by",
277
+ ]
278
+ readonly_fields = fields # type: ignore[assignment]
279
+ extra = 0
280
+
281
+ def get_queryset(self, request):
282
+ return super().get_queryset(request).select_related(
283
+ "publishable_entity_version"
284
+ )
285
+
286
+ def children(self, obj: ContainerVersion):
287
+ return _entity_list_detail_link(obj.entity_list)
288
+
289
+
290
+ @admin.register(Container)
291
+ class ContainerAdmin(ReadOnlyModelAdmin):
292
+ """
293
+ Django admin configuration for Container
294
+ """
295
+ list_display = ("key", "created", "draft", "published", "see_also")
296
+ fields = [
297
+ "pk",
298
+ "publishable_entity",
299
+ "learning_package",
300
+ "draft",
301
+ "published",
302
+ "created",
303
+ "created_by",
304
+ "see_also",
305
+ "most_recent_parent_entity_list",
306
+ ]
307
+ readonly_fields = fields # type: ignore[assignment]
308
+ search_fields = ["publishable_entity__uuid", "publishable_entity__key"]
309
+ inlines = [ContainerVersionInlineForContainer]
310
+
311
+ def learning_package(self, obj: Container) -> SafeText:
312
+ return model_detail_link(
313
+ obj.publishable_entity.learning_package,
314
+ obj.publishable_entity.learning_package.key,
315
+ )
316
+
317
+ def get_queryset(self, request):
318
+ return super().get_queryset(request).select_related(
319
+ "publishable_entity",
320
+ "publishable_entity__learning_package",
321
+ "publishable_entity__published__version",
322
+ "publishable_entity__draft__version",
323
+ )
324
+
325
+ def draft(self, obj: Container) -> str:
326
+ """
327
+ Link to this Container's draft ContainerVersion
328
+ """
329
+ if draft := obj.versioning.draft:
330
+ return format_html(
331
+ 'Version {} "{}" ({})', draft.version_num, draft.title, _entity_list_detail_link(draft.entity_list)
332
+ )
333
+ return "-"
334
+
335
+ def published(self, obj: Container) -> str:
336
+ """
337
+ Link to this Container's published ContainerVersion
338
+ """
339
+ if published := obj.versioning.published:
340
+ return format_html(
341
+ 'Version {} "{}" ({})',
342
+ published.version_num,
343
+ published.title,
344
+ _entity_list_detail_link(published.entity_list),
345
+ )
346
+ return "-"
347
+
348
+ def see_also(self, obj: Container):
349
+ return one_to_one_related_model_html(obj)
350
+
351
+ def most_recent_parent_entity_list(self, obj: Container) -> str:
352
+ if latest_row := EntityListRow.objects.filter(entity_id=obj.publishable_entity_id).order_by("-pk").first():
353
+ return _entity_list_detail_link(latest_row.entity_list)
354
+ return "-"
355
+
356
+
357
+ class ContainerVersionInlineForEntityList(admin.TabularInline):
358
+ """
359
+ Inline admin view of ContainerVersions which use a given EntityList
360
+ """
361
+ model = ContainerVersion
362
+ verbose_name = "Container Version that references this Entity List"
363
+ verbose_name_plural = "Container Versions that reference this Entity List"
364
+ ordering = ["-pk"] # Newest first
365
+ fields = [
366
+ "pk",
367
+ "version_num",
368
+ "container_key",
369
+ "title",
370
+ "created",
371
+ "created_by",
372
+ ]
373
+ readonly_fields = fields # type: ignore[assignment]
374
+ extra = 0
375
+
376
+ def get_queryset(self, request):
377
+ return super().get_queryset(request).select_related(
378
+ "container",
379
+ "container__publishable_entity",
380
+ "publishable_entity_version",
381
+ )
382
+
383
+ def container_key(self, obj: ContainerVersion) -> SafeText:
384
+ return model_detail_link(obj.container, obj.container.key)
385
+
386
+
387
+ class EntityListRowInline(admin.TabularInline):
388
+ """
389
+ Table of entity rows in the entitylist admin
390
+ """
391
+ model = EntityListRow
392
+ readonly_fields = [
393
+ "order_num",
394
+ "pinned_version_num",
395
+ "entity_models",
396
+ "container_models",
397
+ "container_children",
398
+ ]
399
+ fields = readonly_fields # type: ignore[assignment]
400
+
401
+ def get_queryset(self, request):
402
+ return super().get_queryset(request).select_related(
403
+ "entity",
404
+ "entity_version",
405
+ )
406
+
407
+ def pinned_version_num(self, obj: EntityListRow):
408
+ return str(obj.entity_version.version_num) if obj.entity_version else "(Unpinned)"
409
+
410
+ def entity_models(self, obj: EntityListRow):
411
+ return format_html(
412
+ "{}<ul>{}</ul>",
413
+ model_detail_link(obj.entity, obj.entity.key),
414
+ one_to_one_related_model_html(obj.entity),
415
+ )
416
+
417
+ def container_models(self, obj: EntityListRow) -> SafeText:
418
+ if not hasattr(obj.entity, "container"):
419
+ return SafeText("(Not a Container)")
420
+ return format_html(
421
+ "{}<ul>{}</ul>",
422
+ model_detail_link(obj.entity.container, str(obj.entity.container)),
423
+ one_to_one_related_model_html(obj.entity.container),
424
+ )
425
+
426
+ def container_children(self, obj: EntityListRow) -> SafeText:
427
+ """
428
+ If this row holds a Container, then link *its* EntityList, allowing easy hierarchy browsing.
429
+
430
+ When determining which ContainerVersion to grab the EntityList from, prefer the pinned
431
+ version if there is one; otherwise use the Draft version.
432
+ """
433
+ if not hasattr(obj.entity, "container"):
434
+ return SafeText("(Not a Container)")
435
+ child_container_version: ContainerVersion = (
436
+ obj.entity_version.containerversion
437
+ if obj.entity_version
438
+ else obj.entity.container.versioning.draft
439
+ )
440
+ return _entity_list_detail_link(child_container_version.entity_list)
441
+
442
+
443
+ @admin.register(EntityList)
444
+ class EntityListAdmin(ReadOnlyModelAdmin):
445
+ """
446
+ Django admin configuration for EntityList
447
+ """
448
+ list_display = [
449
+ "entity_list",
450
+ "row_count",
451
+ "recent_container_version_num",
452
+ "recent_container",
453
+ "recent_container_package"
454
+ ]
455
+ inlines = [ContainerVersionInlineForEntityList, EntityListRowInline]
456
+
457
+ def entity_list(self, obj: EntityList) -> SafeText:
458
+ return model_detail_link(obj, f"EntityList #{obj.pk}")
459
+
460
+ def row_count(self, obj: EntityList) -> int:
461
+ return obj.entitylistrow_set.count()
462
+
463
+ def recent_container_version_num(self, obj: EntityList) -> str:
464
+ """
465
+ Number of the newest ContainerVersion that references this EntityList
466
+ """
467
+ if latest := _latest_container_version(obj):
468
+ return f"Version {latest.version_num}"
469
+ else:
470
+ return "-"
471
+
472
+ def recent_container(self, obj: EntityList) -> SafeText | None:
473
+ """
474
+ Link to the Container of the newest ContainerVersion that references this EntityList
475
+ """
476
+ if latest := _latest_container_version(obj):
477
+ return format_html("of: {}", model_detail_link(latest.container, latest.container.key))
478
+ else:
479
+ return None
480
+
481
+ def recent_container_package(self, obj: EntityList) -> SafeText | None:
482
+ """
483
+ Link to the LearningPackage of the newest ContainerVersion that references this EntityList
484
+ """
485
+ if latest := _latest_container_version(obj):
486
+ return format_html(
487
+ "in: {}",
488
+ model_detail_link(
489
+ latest.container.publishable_entity.learning_package,
490
+ latest.container.publishable_entity.learning_package.key
491
+ )
492
+ )
493
+ else:
494
+ return None
495
+
496
+ # We'd like it to appear as if these three columns are just a single
497
+ # nicely-formatted column, so only give the left one a description.
498
+ recent_container_version_num.short_description = ( # type: ignore[attr-defined]
499
+ "Most recent container version using this entity list"
500
+ )
501
+ recent_container.short_description = "" # type: ignore[attr-defined]
502
+ recent_container_package.short_description = "" # type: ignore[attr-defined]
503
+
504
+
505
+ @functools.cache
506
+ def _latest_container_version(obj: EntityList) -> ContainerVersion | None:
507
+ """
508
+ Any given EntityList can be used by multiple ContainerVersion (which may even
509
+ span multiple Containers). We only have space here to show one ContainerVersion
510
+ easily, so let's show the one that's most likely to be interesting to the Django
511
+ admin user: the most-recently-created one.
512
+ """
513
+ return obj.container_versions.order_by("-pk").first()
@@ -60,6 +60,7 @@ __all__ = [
60
60
  "get_publishable_entity_by_key",
61
61
  "get_last_publish",
62
62
  "get_all_drafts",
63
+ "get_entities",
63
64
  "get_entities_with_unpublished_changes",
64
65
  "get_entities_with_unpublished_deletes",
65
66
  "publish_all_drafts",
@@ -69,7 +70,7 @@ __all__ = [
69
70
  "set_draft_version",
70
71
  "soft_delete_draft",
71
72
  "reset_drafts_to_published",
72
- "register_content_models",
73
+ "register_publishable_models",
73
74
  "filter_publishable_entities",
74
75
  # 🛑 UNSTABLE: All APIs related to containers are unstable until we've figured
75
76
  # out our approach to dynamic content (randomized, A/B tests, etc.)
@@ -261,6 +262,13 @@ def get_all_drafts(learning_package_id: int, /) -> QuerySet[Draft]:
261
262
  )
262
263
 
263
264
 
265
+ def get_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
266
+ """
267
+ Get all entities in a learning package.
268
+ """
269
+ return PublishableEntity.objects.filter(learning_package_id=learning_package_id)
270
+
271
+
264
272
  def get_entities_with_unpublished_changes(
265
273
  learning_package_id: int,
266
274
  /,
@@ -789,7 +797,7 @@ def reset_drafts_to_published(
789
797
  set_draft_version(draft, published_version_id)
790
798
 
791
799
 
792
- def register_content_models(
800
+ def register_publishable_models(
793
801
  content_model_cls: type[PublishableEntityMixin],
794
802
  content_version_model_cls: type[PublishableEntityVersionMixin],
795
803
  ) -> PublishableContentModelRegistry:
@@ -805,10 +813,10 @@ def register_content_models(
805
813
  method. For example, in the components app, this looks like:
806
814
 
807
815
  def ready(self):
808
- from ..publishing.api import register_content_models
816
+ from ..publishing.api import register_publishable_models
809
817
  from .models import Component, ComponentVersion
810
818
 
811
- register_content_models(Component, ComponentVersion)
819
+ register_publishable_models(Component, ComponentVersion)
812
820
 
813
821
  There may be a more clever way to introspect this information from the model
814
822
  metadata, but this is simple and explicit.
@@ -1275,6 +1283,7 @@ def get_entities_in_container(
1275
1283
  container: Container,
1276
1284
  *,
1277
1285
  published: bool,
1286
+ select_related_version: str | None = None,
1278
1287
  ) -> list[ContainerEntityListEntry]:
1279
1288
  """
1280
1289
  [ 🛑 UNSTABLE ]
@@ -1285,14 +1294,35 @@ def get_entities_in_container(
1285
1294
  container: The Container, e.g. returned by `get_container()`
1286
1295
  published: `True` if we want the published version of the container, or
1287
1296
  `False` for the draft version.
1297
+ select_related_version: An optional optimization; specify a relationship
1298
+ on ContainerVersion, like `componentversion` or `containerversion__x`
1299
+ to preload via select_related.
1288
1300
  """
1289
1301
  assert isinstance(container, Container)
1290
- container_version = container.versioning.published if published else container.versioning.draft
1302
+ if published:
1303
+ # Very minor optimization: reload the container with related 1:1 entities
1304
+ container = Container.objects.select_related(
1305
+ "publishable_entity__published__version__containerversion__entity_list").get(pk=container.pk)
1306
+ container_version = container.versioning.published
1307
+ select_related = ["entity__published__version"]
1308
+ if select_related_version:
1309
+ select_related.append(f"entity__published__version__{select_related_version}")
1310
+ else:
1311
+ # Very minor optimization: reload the container with related 1:1 entities
1312
+ container = Container.objects.select_related(
1313
+ "publishable_entity__draft__version__containerversion__entity_list").get(pk=container.pk)
1314
+ container_version = container.versioning.draft
1315
+ select_related = ["entity__draft__version"]
1316
+ if select_related_version:
1317
+ select_related.append(f"entity__draft__version__{select_related_version}")
1291
1318
  if container_version is None:
1292
1319
  raise ContainerVersion.DoesNotExist # This container has not been published yet, or has been deleted.
1293
1320
  assert isinstance(container_version, ContainerVersion)
1294
- entity_list = []
1295
- for row in container_version.entity_list.entitylistrow_set.order_by("order_num"):
1321
+ entity_list: list[ContainerEntityListEntry] = []
1322
+ for row in container_version.entity_list.entitylistrow_set.select_related(
1323
+ "entity_version",
1324
+ *select_related,
1325
+ ).order_by("order_num"):
1296
1326
  entity_version = row.entity_version # This will be set if pinned
1297
1327
  if not entity_version: # If this entity is "unpinned", use the latest published/draft version:
1298
1328
  entity_version = row.entity.published.version if published else row.entity.draft.version
@@ -1385,7 +1415,10 @@ def get_containers_with_entity(
1385
1415
  qs = Container.objects.filter(
1386
1416
  publishable_entity__draft__version__containerversion__entity_list__entitylistrow__entity_id=publishable_entity_pk, # pylint: disable=line-too-long # noqa: E501
1387
1417
  )
1388
- return qs.order_by("pk").distinct() # Ordering is mostly for consistent test cases.
1418
+ return qs.select_related(
1419
+ "publishable_entity__draft__version__containerversion",
1420
+ "publishable_entity__published__version__containerversion",
1421
+ ).order_by("pk").distinct() # Ordering is mostly for consistent test cases.
1389
1422
 
1390
1423
 
1391
1424
  def get_container_children_count(
@@ -19,7 +19,7 @@ class PublishingConfig(AppConfig):
19
19
  """
20
20
  Register Container and ContainerVersion.
21
21
  """
22
- from .api import register_content_models # pylint: disable=import-outside-toplevel
22
+ from .api import register_publishable_models # pylint: disable=import-outside-toplevel
23
23
  from .models import Container, ContainerVersion # pylint: disable=import-outside-toplevel
24
24
 
25
- register_content_models(Container, ContainerVersion)
25
+ register_publishable_models(Container, ContainerVersion)
@@ -221,6 +221,9 @@ class PublishableEntityVersion(models.Model):
221
221
  blank=True,
222
222
  )
223
223
 
224
+ def __str__(self):
225
+ return f"{self.entity.key} @ v{self.version_num} - {self.title}"
226
+
224
227
  class Meta:
225
228
  constraints = [
226
229
  # Prevent the situation where we have multiple
@@ -269,7 +272,7 @@ class PublishableEntityMixin(models.Model):
269
272
  Please see docstring for PublishableEntity for more details.
270
273
 
271
274
  If you use this class, you *MUST* also use PublishableEntityVersionMixin and
272
- the publishing app's api.register_content_models (see its docstring for
275
+ the publishing app's api.register_publishable_models (see its docstring for
273
276
  details).
274
277
  """
275
278
  # select these related entities by default for all queries
@@ -291,6 +294,10 @@ class PublishableEntityMixin(models.Model):
291
294
  def uuid(self) -> str:
292
295
  return self.publishable_entity.uuid
293
296
 
297
+ @property
298
+ def can_stand_alone(self) -> bool:
299
+ return self.publishable_entity.can_stand_alone
300
+
294
301
  @property
295
302
  def key(self) -> str:
296
303
  return self.publishable_entity.key
@@ -303,6 +310,9 @@ class PublishableEntityMixin(models.Model):
303
310
  def created_by(self):
304
311
  return self.publishable_entity.created_by
305
312
 
313
+ def __str__(self) -> str:
314
+ return str(self.publishable_entity)
315
+
306
316
  class Meta:
307
317
  abstract = True
308
318
 
@@ -545,7 +555,7 @@ class PublishableEntityVersionMixin(models.Model):
545
555
  Please see docstring for PublishableEntityVersion for more details.
546
556
 
547
557
  If you use this class, you *MUST* also use PublishableEntityMixin and the
548
- publishing app's api.register_content_models (see its docstring for
558
+ publishing app's api.register_publishable_models (see its docstring for
549
559
  details).
550
560
  """
551
561
 
@@ -570,10 +580,17 @@ class PublishableEntityVersionMixin(models.Model):
570
580
  def created(self) -> datetime:
571
581
  return self.publishable_entity_version.created
572
582
 
583
+ @property
584
+ def created_by(self):
585
+ return self.publishable_entity_version.created_by
586
+
573
587
  @property
574
588
  def version_num(self) -> int:
575
589
  return self.publishable_entity_version.version_num
576
590
 
591
+ def __str__(self) -> str:
592
+ return str(self.publishable_entity_version)
593
+
577
594
  class Meta:
578
595
  abstract = True
579
596
 
@@ -596,7 +613,7 @@ class PublishableContentModelRegistry:
596
613
  Register what content model maps to what content version model.
597
614
 
598
615
  If you want to call this from another app, please use the
599
- ``register_content_models`` function in this app's ``api`` module
616
+ ``register_publishable_models`` function in this app's ``api`` module
600
617
  instead.
601
618
  """
602
619
  if not issubclass(content_model_cls, PublishableEntityMixin):
@@ -0,0 +1,48 @@
1
+ """
2
+ Django admin for sections models
3
+ """
4
+ from django.contrib import admin
5
+ from django.utils.safestring import SafeText
6
+
7
+ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, model_detail_link
8
+
9
+ from ..publishing.models import ContainerVersion
10
+ from .models import Section, SectionVersion
11
+
12
+
13
+ class SectionVersionInline(admin.TabularInline):
14
+ """
15
+ Minimal table for section versions in a section.
16
+
17
+ (Generally, this information is useless, because each SectionVersion should have a
18
+ matching ContainerVersion, shown in much more detail on the Container detail page.
19
+ But we've hit at least one bug where ContainerVersions were being created without
20
+ their connected SectionVersions, so we'll leave this table here for debugging
21
+ at least until we've made the APIs more robust against that sort of data corruption.)
22
+ """
23
+ model = SectionVersion
24
+ fields = ["pk"]
25
+ readonly_fields = ["pk"]
26
+ ordering = ["-pk"] # Newest first
27
+
28
+ def pk(self, obj: ContainerVersion) -> SafeText:
29
+ return obj.pk
30
+
31
+
32
+ @admin.register(Section)
33
+ class SectionAdmin(ReadOnlyModelAdmin):
34
+ """
35
+ Very minimal interface... just direct the admin user's attention towards the related Container model admin.
36
+ """
37
+ inlines = [SectionVersionInline]
38
+ list_display = ["pk", "key"]
39
+ fields = ["key"]
40
+ readonly_fields = ["key"]
41
+
42
+ def key(self, obj: Section) -> SafeText:
43
+ return model_detail_link(obj.container, obj.key)
44
+
45
+ def get_form(self, request, obj=None, change=False, **kwargs):
46
+ help_texts = {'key': f'For more details of this {self.model.__name__}, click above to see its Container view'}
47
+ kwargs.update({'help_texts': help_texts})
48
+ return super().get_form(request, obj, **kwargs)
@@ -24,7 +24,6 @@ __all__ = [
24
24
  "get_latest_section_version",
25
25
  "SectionListEntry",
26
26
  "get_subsections_in_section",
27
- "get_subsections_in_section",
28
27
  "get_subsections_in_published_section_as_of",
29
28
  ]
30
29
 
@@ -258,7 +257,12 @@ def get_subsections_in_section(
258
257
  """
259
258
  assert isinstance(section, Section)
260
259
  subsections = []
261
- for entry in publishing_api.get_entities_in_container(section, published=published):
260
+ entries = publishing_api.get_entities_in_container(
261
+ section,
262
+ published=published,
263
+ select_related_version="containerversion__subsectionversion",
264
+ )
265
+ for entry in entries:
262
266
  # Convert from generic PublishableEntityVersion to SubsectionVersion:
263
267
  subsection_version = entry.entity_version.containerversion.subsectionversion
264
268
  assert isinstance(subsection_version, SubsectionVersion)
@@ -1,5 +1,5 @@
1
1
  """
2
- Subsection Django application initialization.
2
+ Sections Django application initialization.
3
3
  """
4
4
 
5
5
  from django.apps import AppConfig
@@ -7,7 +7,7 @@ from django.apps import AppConfig
7
7
 
8
8
  class SectionsConfig(AppConfig):
9
9
  """
10
- Configuration for the subsections Django application.
10
+ Configuration for the Sections Django application.
11
11
  """
12
12
 
13
13
  name = "openedx_learning.apps.authoring.sections"
@@ -19,7 +19,7 @@ class SectionsConfig(AppConfig):
19
19
  """
20
20
  Register Section and SectionVersion.
21
21
  """
22
- from ..publishing.api import register_content_models # pylint: disable=import-outside-toplevel
22
+ from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
23
23
  from .models import Section, SectionVersion # pylint: disable=import-outside-toplevel
24
24
 
25
- register_content_models(Section, SectionVersion)
25
+ register_publishable_models(Section, SectionVersion)
@@ -0,0 +1,48 @@
1
+ """
2
+ Django admin for subsection models
3
+ """
4
+ from django.contrib import admin
5
+ from django.utils.safestring import SafeText
6
+
7
+ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, model_detail_link
8
+
9
+ from ..publishing.models import ContainerVersion
10
+ from .models import Subsection, SubsectionVersion
11
+
12
+
13
+ class SubsectionVersionInline(admin.TabularInline):
14
+ """
15
+ Minimal table for subsection versions in a subsection.
16
+
17
+ (Generally, this information is useless, because each SubsectionVersion should have a
18
+ matching ContainerVersion, shown in much more detail on the Container detail page.
19
+ But we've hit at least one bug where ContainerVersions were being created without
20
+ their connected SubsectionVersions, so we'll leave this table here for debugging
21
+ at least until we've made the APIs more robust against that sort of data corruption.)
22
+ """
23
+ model = SubsectionVersion
24
+ fields = ["pk"]
25
+ readonly_fields = ["pk"]
26
+ ordering = ["-pk"] # Newest first
27
+
28
+ def pk(self, obj: ContainerVersion) -> SafeText:
29
+ return obj.pk
30
+
31
+
32
+ @admin.register(Subsection)
33
+ class SubsectionAdmin(ReadOnlyModelAdmin):
34
+ """
35
+ Very minimal interface... just direct the admin user's attention towards the related Container model admin.
36
+ """
37
+ inlines = [SubsectionVersionInline]
38
+ list_display = ["pk", "key"]
39
+ fields = ["key"]
40
+ readonly_fields = ["key"]
41
+
42
+ def key(self, obj: Subsection) -> SafeText:
43
+ return model_detail_link(obj.container, obj.key)
44
+
45
+ def get_form(self, request, obj=None, change=False, **kwargs):
46
+ help_texts = {'key': f'For more details of this {self.model.__name__}, click above to see its Container view'}
47
+ kwargs.update({'help_texts': help_texts})
48
+ return super().get_form(request, obj, **kwargs)
@@ -24,7 +24,6 @@ __all__ = [
24
24
  "get_latest_subsection_version",
25
25
  "SubsectionListEntry",
26
26
  "get_units_in_subsection",
27
- "get_units_in_subsection",
28
27
  "get_units_in_published_subsection_as_of",
29
28
  ]
30
29
 
@@ -257,7 +256,12 @@ def get_units_in_subsection(
257
256
  """
258
257
  assert isinstance(subsection, Subsection)
259
258
  units = []
260
- for entry in publishing_api.get_entities_in_container(subsection, published=published):
259
+ entries = publishing_api.get_entities_in_container(
260
+ subsection,
261
+ published=published,
262
+ select_related_version="containerversion__unitversion",
263
+ )
264
+ for entry in entries:
261
265
  # Convert from generic PublishableEntityVersion to UnitVersion:
262
266
  unit_version = entry.entity_version.containerversion.unitversion
263
267
  assert isinstance(unit_version, UnitVersion)
@@ -19,7 +19,7 @@ class SubsectionsConfig(AppConfig):
19
19
  """
20
20
  Register Subsection and SubsectionVersion.
21
21
  """
22
- from ..publishing.api import register_content_models # pylint: disable=import-outside-toplevel
22
+ from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
23
23
  from .models import Subsection, SubsectionVersion # pylint: disable=import-outside-toplevel
24
24
 
25
- register_content_models(Subsection, SubsectionVersion)
25
+ register_publishable_models(Subsection, SubsectionVersion)
@@ -0,0 +1,48 @@
1
+ """
2
+ Django admin for units models
3
+ """
4
+ from django.contrib import admin
5
+ from django.utils.safestring import SafeText
6
+
7
+ from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, model_detail_link
8
+
9
+ from ..publishing.models import ContainerVersion
10
+ from .models import Unit, UnitVersion
11
+
12
+
13
+ class UnitVersionInline(admin.TabularInline):
14
+ """
15
+ Minimal table for unit versions in a unit
16
+
17
+ (Generally, this information is useless, because each UnitVersion should have a
18
+ matching ContainerVersion, shown in much more detail on the Container detail page.
19
+ But we've hit at least one bug where ContainerVersions were being created without
20
+ their connected UnitVersions, so we'll leave this table here for debugging
21
+ at least until we've made the APIs more robust against that sort of data corruption.)
22
+ """
23
+ model = UnitVersion
24
+ fields = ["pk"]
25
+ readonly_fields = ["pk"]
26
+ ordering = ["-pk"] # Newest first
27
+
28
+ def pk(self, obj: ContainerVersion) -> SafeText:
29
+ return obj.pk
30
+
31
+
32
+ @admin.register(Unit)
33
+ class UnitAdmin(ReadOnlyModelAdmin):
34
+ """
35
+ Very minimal interface... just direct the admin user's attention towards the related Container model admin.
36
+ """
37
+ inlines = [UnitVersionInline]
38
+ list_display = ["pk", "key"]
39
+ fields = ["key"]
40
+ readonly_fields = ["key"]
41
+
42
+ def key(self, obj: Unit) -> SafeText:
43
+ return model_detail_link(obj.container, obj.key)
44
+
45
+ def get_form(self, request, obj=None, change=False, **kwargs):
46
+ help_texts = {'key': f'For more details of this {self.model.__name__}, click above to see its Container view'}
47
+ kwargs.update({'help_texts': help_texts})
48
+ return super().get_form(request, obj, **kwargs)
@@ -24,7 +24,6 @@ __all__ = [
24
24
  "get_latest_unit_version",
25
25
  "UnitListEntry",
26
26
  "get_components_in_unit",
27
- "get_components_in_unit",
28
27
  "get_components_in_published_unit_as_of",
29
28
  ]
30
29
 
@@ -258,7 +257,12 @@ def get_components_in_unit(
258
257
  """
259
258
  assert isinstance(unit, Unit)
260
259
  components = []
261
- for entry in publishing_api.get_entities_in_container(unit, published=published):
260
+ entries = publishing_api.get_entities_in_container(
261
+ unit,
262
+ published=published,
263
+ select_related_version="componentversion",
264
+ )
265
+ for entry in entries:
262
266
  # Convert from generic PublishableEntityVersion to ComponentVersion:
263
267
  component_version = entry.entity_version.componentversion
264
268
  assert isinstance(component_version, ComponentVersion)
@@ -19,7 +19,7 @@ class UnitsConfig(AppConfig):
19
19
  """
20
20
  Register Unit and UnitVersion.
21
21
  """
22
- from ..publishing.api import register_content_models # pylint: disable=import-outside-toplevel
22
+ from ..publishing.api import register_publishable_models # pylint: disable=import-outside-toplevel
23
23
  from .models import Unit, UnitVersion # pylint: disable=import-outside-toplevel
24
24
 
25
- register_content_models(Unit, UnitVersion)
25
+ register_publishable_models(Unit, UnitVersion)
@@ -2,9 +2,11 @@
2
2
  Convenience utilities for the Django Admin.
3
3
  """
4
4
  from django.contrib import admin
5
+ from django.db import models
5
6
  from django.db.models.fields.reverse_related import OneToOneRel
6
7
  from django.urls import NoReverseMatch, reverse
7
8
  from django.utils.html import format_html, format_html_join
9
+ from django.utils.safestring import SafeText
8
10
 
9
11
 
10
12
  class ReadOnlyModelAdmin(admin.ModelAdmin):
@@ -31,7 +33,7 @@ class ReadOnlyModelAdmin(admin.ModelAdmin):
31
33
  return False
32
34
 
33
35
 
34
- def one_to_one_related_model_html(model_obj):
36
+ def one_to_one_related_model_html(model_obj: models.Model) -> SafeText:
35
37
  """
36
38
  HTML for clickable list of a models that are 1:1-related to ``model_obj``.
37
39
 
@@ -98,3 +100,17 @@ def one_to_one_related_model_html(model_obj):
98
100
  text.append(html)
99
101
 
100
102
  return format_html_join("\n", "<li>{}</li>", ((t,) for t in text))
103
+
104
+
105
+ def model_detail_link(obj: models.Model, link_text: str) -> SafeText:
106
+ """
107
+ Render an HTML link to the admin focus page for `obj`.
108
+ """
109
+ return format_html(
110
+ '<a href="{}">{}</a>',
111
+ reverse(
112
+ f"admin:{obj._meta.app_label}_{(obj._meta.model_name or obj.__class__.__name__).lower()}_change",
113
+ args=(obj.pk,),
114
+ ),
115
+ link_text,
116
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-learning
3
- Version: 0.26.0
3
+ Version: 0.27.1
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -20,10 +20,11 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Requires-Python: >=3.11
21
21
  License-File: LICENSE.txt
22
22
  Requires-Dist: djangorestframework<4.0
23
+ Requires-Dist: tomlkit
23
24
  Requires-Dist: celery
24
- Requires-Dist: edx-drf-extensions
25
- Requires-Dist: attrs
26
25
  Requires-Dist: Django
26
+ Requires-Dist: attrs
27
+ Requires-Dist: edx-drf-extensions
27
28
  Requires-Dist: rules<4.0
28
29
  Dynamic: author
29
30
  Dynamic: author-email
@@ -1,10 +1,21 @@
1
- openedx_learning/__init__.py,sha256=gxlH6AqH1wFwipo5FfaLp8R8Uxg7pp57boi7X8I4LT0,69
1
+ openedx_learning/__init__.py,sha256=yNCuSKMLv6YJnAu09cqc4GpR5uVIDaJP2-5z9RAWJeM,69
2
2
  openedx_learning/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  openedx_learning/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- openedx_learning/api/authoring.py,sha256=1sh-hUH3pJLVIQHzzjlqWb_9uxf9y3-hanLpU4mRvXc,1061
4
+ openedx_learning/api/authoring.py,sha256=EDWTY_JDKtjD9nFrrijzWuVccs3LZeDLEdzTUNanR4I,1111
5
5
  openedx_learning/api/authoring_models.py,sha256=lA500-C7LBlVzeaEVqmCiQMAPPmeoDMi8wS0TbzgdGw,778
6
6
  openedx_learning/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  openedx_learning/apps/authoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ openedx_learning/apps/authoring/backup_restore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ openedx_learning/apps/authoring/backup_restore/admin.py,sha256=OnEixkOuysPRr-F6C_CMwPkiXawkqgSEF46n3yiUK0o,59
10
+ openedx_learning/apps/authoring/backup_restore/api.py,sha256=zEns3crvfFEFFh7MmwzSqW0WuGmZaSgdmujzl0PnfvU,508
11
+ openedx_learning/apps/authoring/backup_restore/apps.py,sha256=UnExBA7jhd3qI30_87JMvzVhS_k82t89qDVKSMpvg_A,340
12
+ openedx_learning/apps/authoring/backup_restore/models.py,sha256=jlr0ppxW0IOW3HPHoJNChHvDrYVnKMb5_3uC2itxqQk,45
13
+ openedx_learning/apps/authoring/backup_restore/toml.py,sha256=KRlOSln1wNB6U7qXXf6Cr9_GZrLUXLwH-x9Yxd9lmBk,2874
14
+ openedx_learning/apps/authoring/backup_restore/zipper.py,sha256=6hX8qQFHS3HCAUs5JsxHuLUWOOa_6Y8c93WcAZKDE4A,2379
15
+ openedx_learning/apps/authoring/backup_restore/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py,sha256=TkbyBf9Jsa7yoXiGEduO0ZqKTYO7vWGHbqr5NbEclRs,1696
18
+ openedx_learning/apps/authoring/backup_restore/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
19
  openedx_learning/apps/authoring/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
20
  openedx_learning/apps/authoring/collections/admin.py,sha256=f0hySjDMfIphVDEGkCSMIUHoEiqHRA7EE2NiO7lvL4g,1156
10
21
  openedx_learning/apps/authoring/collections/api.py,sha256=DaGg73iom7fN9fODajo8B2e9Jkx2syfLEVjip0cAzlQ,7747
@@ -19,7 +30,7 @@ openedx_learning/apps/authoring/collections/migrations/__init__.py,sha256=47DEQp
19
30
  openedx_learning/apps/authoring/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
31
  openedx_learning/apps/authoring/components/admin.py,sha256=zfEpuBEySMYpUZzygaE2MDoI8SH-2H3xIL20YCSCMLo,4582
21
32
  openedx_learning/apps/authoring/components/api.py,sha256=nJZcGXN5gOz8EYX3XHCiDILowNRcm__RxTfTNt5rZZw,22780
22
- openedx_learning/apps/authoring/components/apps.py,sha256=Rcydv_FH-rVvuWIFqUezBNOh8DtrZHyozZM2yqX2JKE,778
33
+ openedx_learning/apps/authoring/components/apps.py,sha256=hi1SF2Z8Ex0hgE82wJK5Z_vYYfbcRhtaUW1zWZCdJYI,786
23
34
  openedx_learning/apps/authoring/components/models.py,sha256=ttZzVnMZTa14-qudrLb4CFuCanEQJT8cuC_iVPH8XTA,10887
24
35
  openedx_learning/apps/authoring/components/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
36
  openedx_learning/apps/authoring/components/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,9 +47,9 @@ openedx_learning/apps/authoring/contents/models.py,sha256=8CYrHK7CZ4U3F7Den4ZV_a
36
47
  openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTmIGX2KHpjw-PHbfRjxkFEomI5CEDhNKCZ7IpFeE,3060
37
48
  openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
49
  openedx_learning/apps/authoring/publishing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- openedx_learning/apps/authoring/publishing/admin.py,sha256=sLmtttaYIQi8XWw_wOdDD9xbF34g1giMEj5yIN-b-4M,7012
40
- openedx_learning/apps/authoring/publishing/api.py,sha256=BQ5Q0JsSRBmb8ZRKzUlEOOV6QxNUjYkkqCjxmnX9O48,55565
41
- openedx_learning/apps/authoring/publishing/apps.py,sha256=v9PTe3YoICaYT9wfu268ZkVAlnZFvxi-DqYdbRi25bY,750
50
+ openedx_learning/apps/authoring/publishing/admin.py,sha256=nvAAl3Xswqqq3WyaI1NT7pLCcu1o-ynciJZOlc-9L24,16244
51
+ openedx_learning/apps/authoring/publishing/api.py,sha256=rLwS_Ma1Yp5bryR5-gTllIot6AjFB5g-7UFUxZtJsPU,57208
52
+ openedx_learning/apps/authoring/publishing/apps.py,sha256=PXYIx-TwN7a8dDudodX80Z7hNV9bWzrMZnpDET8lCGE,758
42
53
  openedx_learning/apps/authoring/publishing/contextmanagers.py,sha256=AH5zhr0Tz_gUG9--dfr_oZAu8DMy94n6mnOJuPbWkeU,6723
43
54
  openedx_learning/apps/authoring/publishing/migrations/0001_initial.py,sha256=wvekNV19YRSdxRmQaFnLSn_nCsQlHIucPDVMmgKf_OE,9272
44
55
  openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py,sha256=toI7qJhNukk6hirKfFx9EpqTpzF2O2Yq1VpFJusDn2M,806
@@ -55,22 +66,25 @@ openedx_learning/apps/authoring/publishing/models/draft_log.py,sha256=7hpbtnnc3y
55
66
  openedx_learning/apps/authoring/publishing/models/entity_list.py,sha256=8MyJqDdC8dqJY-N9UAu-WS-ZeFOYuRNMKloSY9wMH1w,3042
56
67
  openedx_learning/apps/authoring/publishing/models/learning_package.py,sha256=1fuNLHD6k0qGuL0jXYGf4-TA5WczgxJrXUdAIM_JNBI,2688
57
68
  openedx_learning/apps/authoring/publishing/models/publish_log.py,sha256=QD7Fb00yUMWM4HHae_m60JW-TDX8upbQdWCEIdhB0yE,5615
58
- openedx_learning/apps/authoring/publishing/models/publishable_entity.py,sha256=-6Fde_sYqaq1AUC4sw4Rf1X9Hh4jun2Py7Xhsl4VWuY,25419
69
+ openedx_learning/apps/authoring/publishing/models/publishable_entity.py,sha256=ErzsvCcYbvjnMvsPqErOgSob9Vpaa7nmykNPTSQkZk8,25894
59
70
  openedx_learning/apps/authoring/sections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- openedx_learning/apps/authoring/sections/api.py,sha256=cwsxfHZa6fY_GyC4gSvSRAjayNQWKGY9DWLAyJ_u8Oc,10357
61
- openedx_learning/apps/authoring/sections/apps.py,sha256=iamzAiVlAWXGlZExP_bKSn38u4mUF8H0-Pa7eFtAkro,743
71
+ openedx_learning/apps/authoring/sections/admin.py,sha256=OQOTtXYM-Zj8BBb1wNBkOxgkF2Pv3JdUrZ45VOEmThM,1757
72
+ openedx_learning/apps/authoring/sections/api.py,sha256=MFAHxDqUqbXygY_L37Lo4uXrju0m4Kf9ozk4yZnDs6Y,10438
73
+ openedx_learning/apps/authoring/sections/apps.py,sha256=vbLhC3WIKmG1MD0mvxX01IjoF6xPiJoutJar-h_bH7Y,746
62
74
  openedx_learning/apps/authoring/sections/models.py,sha256=2GK-dDMJwNRw_9gNFho8iKcDV-iYz_zBzqGMDmQ_jbc,1450
63
75
  openedx_learning/apps/authoring/sections/migrations/0001_initial.py,sha256=iW5AFhC26mfZNWEVNu8cTsr32Ca4htL4CUanHoXfaeY,1152
64
76
  openedx_learning/apps/authoring/sections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
77
  openedx_learning/apps/authoring/subsections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- openedx_learning/apps/authoring/subsections/api.py,sha256=Cgjq3Y6ZZTzIQXMX6p7Y4jhFwIBFwKhSY_HCTeziMGc,10260
67
- openedx_learning/apps/authoring/subsections/apps.py,sha256=WueCaPOE-7x3cu-6rA9FdeKzipCZSNIhvqpAbxTysOg,773
78
+ openedx_learning/apps/authoring/subsections/admin.py,sha256=vPfOeTzh10aRhtZjXAzYsbwfw4Hc5yuySbpjAvtDH98,1795
79
+ openedx_learning/apps/authoring/subsections/api.py,sha256=cJHPZ2JbAL3aXa1rhV6R5_oIP_VmWWDCi9L-2qjifwo,10338
80
+ openedx_learning/apps/authoring/subsections/apps.py,sha256=awpHVtg6bwIF1sEMeVcaGMwvrVzckfEHOFA9eFt5900,781
68
81
  openedx_learning/apps/authoring/subsections/models.py,sha256=1uhdpS9Eg6keSqkzQaE8-XSVLAQlmi0llIIU2V7Nl44,1492
69
82
  openedx_learning/apps/authoring/subsections/migrations/0001_initial.py,sha256=7kEHIC-EwG2KvlW4hg5tnl45--dW4Yv5gqV5SDqNYNo,1158
70
83
  openedx_learning/apps/authoring/subsections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
84
  openedx_learning/apps/authoring/units/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- openedx_learning/apps/authoring/units/api.py,sha256=KJA7Bh7IBE22E6cMSZNp6pamjjA1Vf8EhHw0rvQtlPM,9797
73
- openedx_learning/apps/authoring/units/apps.py,sha256=cIzphjDw5sjIZ3NLE911N7IMUa8JQSXMReNl03uI7jg,701
85
+ openedx_learning/apps/authoring/units/admin.py,sha256=chp-bTfufBiQ3uycVF1DBEFSPvwXaROJnyyY8AaH_yw,1717
86
+ openedx_learning/apps/authoring/units/api.py,sha256=vmNdXwI4n-ksDu_nD3b-U0ZyQa0mGaJ3cuaj882HU2s,9863
87
+ openedx_learning/apps/authoring/units/apps.py,sha256=AlKOUoC5zPrRrEedLvGzMf31ujWyhcaaoCNS4LI-u50,709
74
88
  openedx_learning/apps/authoring/units/models.py,sha256=eTOwFWC9coQLf0ovx08Mj7zi8mPAWCw9QOznybajRk0,1418
75
89
  openedx_learning/apps/authoring/units/migrations/0001_initial.py,sha256=qM_0JGffxECVgXzncHXfgSE-g8u3L3a14R0M1Bnj_Ys,1129
76
90
  openedx_learning/apps/authoring/units/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -80,14 +94,14 @@ openedx_learning/contrib/media_server/apps.py,sha256=GicFBN3N6wzVs5i3RgrQFJZeMlS
80
94
  openedx_learning/contrib/media_server/urls.py,sha256=newNjV41sM9A9Oy_rgnZSXdkTFxSHiupIiAsVIGE2CE,365
81
95
  openedx_learning/contrib/media_server/views.py,sha256=qZPhdEW_oYj1MEdgLVP6Cq3tRiZtp7dTb7ASaSKZ2HY,1350
82
96
  openedx_learning/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- openedx_learning/lib/admin_utils.py,sha256=5z9NrXxmT5j8azx9u1t0AgxV5PIDTc2jPyM5z5yW8cw,4021
97
+ openedx_learning/lib/admin_utils.py,sha256=dQ02NrgXIJL6kx9LFBgEYc1Pr_xtFdr7NVXx01VOIE0,4514
84
98
  openedx_learning/lib/cache.py,sha256=ppT36KiPLdsAF3GfZCF0IdiHodckd2gLiF1sNhjSJuk,958
85
99
  openedx_learning/lib/collations.py,sha256=f65575r3BfAvFWU6pdBMsqrxPwFijB2SbJtDXq4UVc4,2401
86
100
  openedx_learning/lib/fields.py,sha256=eiGoXMPhRuq25EH2qf6BAODshAQE3DBVdIYAMIUAXW0,7522
87
101
  openedx_learning/lib/managers.py,sha256=-Q3gxalSqyPZ9Im4DTROW5tF8wVTZLlmfTe62_xmowY,1643
88
102
  openedx_learning/lib/test_utils.py,sha256=g3KLuepIZbaDBCsaj9711YuqyUx7LD4gXDcfNC-mWdc,527
89
103
  openedx_learning/lib/validators.py,sha256=iqEdEAvFV2tC7Ecssx69kjecpdU8nE87AlDJYrqrsnc,404
90
- openedx_learning-0.26.0.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
104
+ openedx_learning-0.27.1.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
91
105
  openedx_tagging/__init__.py,sha256=V9N8M7f9LYlAbA_DdPUsHzTnWjYRXKGa5qHw9P1JnNI,30
92
106
  openedx_tagging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
107
  openedx_tagging/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -129,7 +143,7 @@ openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py,sha25
129
143
  openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py,sha256=zmr4b65T0vX6fYc8MpvSmQnYkAiNMpx3RKEd5tudsl8,517
130
144
  openedx_tagging/core/tagging/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
145
  openedx_tagging/core/tagging/models/__init__.py,sha256=yYdOnthuc7EUdfEULtZgqRwn5Y4bbYQmJCjVZqR5GTM,236
132
- openedx_tagging/core/tagging/models/base.py,sha256=RXv4jDncV-JPXMAb62dGYNJhMdiS6kSo24eoG7UeKR4,39655
146
+ openedx_tagging/core/tagging/models/base.py,sha256=ju4mvgRS_I2AgPsRf4sMFy6qle2i0aA0MbyBYZXf32g,39685
133
147
  openedx_tagging/core/tagging/models/import_export.py,sha256=Aj0pleh0nh2LNS6zmdB1P4bpdgUMmvmobTkqBerORAI,4570
134
148
  openedx_tagging/core/tagging/models/system_defined.py,sha256=_6LfvUZGEltvQMtm2OXy6TOLh3C8GnVTqtZDSAZW6K4,9062
135
149
  openedx_tagging/core/tagging/models/utils.py,sha256=-A3Dj24twmTf65UB7G4WLvb_9qEvduEPIwahZ-FJDlg,1926
@@ -143,7 +157,7 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=0HQD_Jrf6-YpocYfz
143
157
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
144
158
  openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=Hf92cy-tE767DE9FgsZcPKiCYrf5ihfETz8qGKBnuiU,36278
145
159
  openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
146
- openedx_learning-0.26.0.dist-info/METADATA,sha256=c8OUFRhcuUhOqInl5uLmPhAutB08-Z0zbjhWw5j1br8,9032
147
- openedx_learning-0.26.0.dist-info/WHEEL,sha256=XAkygS4h1cf0JYWV13kJhTWht2y9NqKAsZuiTHc2920,109
148
- openedx_learning-0.26.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
149
- openedx_learning-0.26.0.dist-info/RECORD,,
160
+ openedx_learning-0.27.1.dist-info/METADATA,sha256=0otM32S0-1Xf6yX0mjj-P2xAOiU_k5VhIQtVpL1WMos,9055
161
+ openedx_learning-0.27.1.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
162
+ openedx_learning-0.27.1.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
163
+ openedx_learning-0.27.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -509,7 +509,7 @@ class Taxonomy(models.Model):
509
509
  count=models.Func(F('id'), function='Count')
510
510
  )
511
511
  qs = qs.annotate(usage_count=models.Subquery(obj_tags.values('count')))
512
- return qs
512
+ return qs # type: ignore[return-value]
513
513
 
514
514
  def _get_filtered_tags_deep(
515
515
  self,