openedx-learning 0.26.0__py2.py3-none-any.whl → 0.27.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.
Files changed (27) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/apps/authoring/backup_restore/__init__.py +0 -0
  3. openedx_learning/apps/authoring/backup_restore/admin.py +3 -0
  4. openedx_learning/apps/authoring/backup_restore/api.py +25 -0
  5. openedx_learning/apps/authoring/backup_restore/apps.py +12 -0
  6. openedx_learning/apps/authoring/backup_restore/management/__init__.py +0 -0
  7. openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py +0 -0
  8. openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +45 -0
  9. openedx_learning/apps/authoring/backup_restore/migrations/__init__.py +0 -0
  10. openedx_learning/apps/authoring/backup_restore/models.py +3 -0
  11. openedx_learning/apps/authoring/backup_restore/toml.py +72 -0
  12. openedx_learning/apps/authoring/publishing/admin.py +277 -12
  13. openedx_learning/apps/authoring/publishing/models/publishable_entity.py +13 -0
  14. openedx_learning/apps/authoring/sections/admin.py +48 -0
  15. openedx_learning/apps/authoring/sections/api.py +0 -1
  16. openedx_learning/apps/authoring/sections/apps.py +2 -2
  17. openedx_learning/apps/authoring/subsections/admin.py +48 -0
  18. openedx_learning/apps/authoring/subsections/api.py +0 -1
  19. openedx_learning/apps/authoring/units/admin.py +48 -0
  20. openedx_learning/apps/authoring/units/api.py +0 -1
  21. openedx_learning/lib/admin_utils.py +17 -1
  22. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.0.dist-info}/METADATA +6 -5
  23. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.0.dist-info}/RECORD +27 -14
  24. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.0.dist-info}/WHEEL +1 -1
  25. openedx_tagging/core/tagging/models/base.py +1 -1
  26. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.0.dist-info}/licenses/LICENSE.txt +0 -0
  27. {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.0.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.0"
@@ -0,0 +1,3 @@
1
+ """
2
+ Django Admin pages for Backup Restore models (WIP)
3
+ """
@@ -0,0 +1,25 @@
1
+ """
2
+ Backup Restore API
3
+ """
4
+ import zipfile
5
+
6
+ from openedx_learning.apps.authoring.publishing.api import get_learning_package_by_key
7
+
8
+ from .toml import TOMLLearningPackageFile
9
+
10
+ TOML_PACKAGE_NAME = "package.toml"
11
+
12
+
13
+ def create_zip_file(lp_key: str, path: str) -> None:
14
+ """
15
+ Creates a zip file with a toml file so far (WIP)
16
+
17
+ Can throw a NotFoundError at get_learning_package_by_key
18
+ """
19
+ learning_package = get_learning_package_by_key(lp_key)
20
+ toml_file = TOMLLearningPackageFile(learning_package)
21
+ toml_file.create()
22
+ toml_content: str = toml_file.get()
23
+ with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as zipf:
24
+ # Add the TOML string as a file in the ZIP
25
+ zipf.writestr(TOML_PACKAGE_NAME, toml_content)
@@ -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,45 @@
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
+ logger.exception(message)
37
+ raise CommandError(message) from exc
38
+ except Exception as e:
39
+ message = f"Failed to export learning package '{lp_key}': {e}"
40
+ logger.exception(
41
+ "Failed to create zip file %s (learning‑package key %s)",
42
+ file_name,
43
+ lp_key,
44
+ )
45
+ raise CommandError(message) from e
@@ -0,0 +1,3 @@
1
+ """
2
+ Core models for Backup Restore (WIP)
3
+ """
@@ -0,0 +1,72 @@
1
+ """
2
+ Utilities for backup and restore app
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Any, Dict
7
+
8
+ from tomlkit import comment, document, dumps, nl, table
9
+ from tomlkit.items import Table
10
+
11
+ from openedx_learning.apps.authoring.publishing.models.learning_package import LearningPackage
12
+
13
+
14
+ class TOMLLearningPackageFile():
15
+ """
16
+ Class to create a .toml representation of a LearningPackage instance.
17
+
18
+ This class builds a structured TOML document using `tomlkit` with metadata and fields
19
+ extracted from a `LearningPackage` object. The output can later be saved to a file or used elsewhere.
20
+ """
21
+
22
+ def __init__(self, learning_package: LearningPackage):
23
+ self.doc = document()
24
+ self.learning_package = learning_package
25
+
26
+ def _create_header(self) -> None:
27
+ """
28
+ Adds a comment with the current datetime to indicate when the export occurred.
29
+ This helps with traceability and file versioning.
30
+ """
31
+ self.doc.add(comment(f"Datetime of the export: {datetime.now()}"))
32
+ self.doc.add(nl())
33
+
34
+ def _create_table(self, params: Dict[str, Any]) -> Table:
35
+ """
36
+ Builds a TOML table section from a dictionary of key-value pairs.
37
+
38
+ Args:
39
+ params (Dict[str, Any]): A dictionary containing keys and values to include in the TOML table.
40
+
41
+ Returns:
42
+ Table: A TOML table populated with the provided keys and values.
43
+ """
44
+ section = table()
45
+ for key, value in params.items():
46
+ section.add(key, value)
47
+ return section
48
+
49
+ def create(self) -> None:
50
+ """
51
+ Populates the TOML document with a header and a table containing
52
+ metadata from the LearningPackage instance.
53
+
54
+ This method must be called before calling `get()`, otherwise the document will be empty.
55
+ """
56
+ self._create_header()
57
+ section = self._create_table({
58
+ "title": self.learning_package.title,
59
+ "key": self.learning_package.key,
60
+ "description": self.learning_package.description,
61
+ "created": self.learning_package.created,
62
+ "updated": self.learning_package.updated
63
+ })
64
+ self.doc.add("learning_package", section)
65
+
66
+ def get(self) -> str:
67
+ """
68
+ Returns:
69
+ str: The string representation of the generated TOML document.
70
+ Ensure `create()` has been called beforehand to get meaningful output.
71
+ """
72
+ return dumps(self.doc)
@@ -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()
@@ -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
@@ -303,6 +306,9 @@ class PublishableEntityMixin(models.Model):
303
306
  def created_by(self):
304
307
  return self.publishable_entity.created_by
305
308
 
309
+ def __str__(self) -> str:
310
+ return str(self.publishable_entity)
311
+
306
312
  class Meta:
307
313
  abstract = True
308
314
 
@@ -570,10 +576,17 @@ class PublishableEntityVersionMixin(models.Model):
570
576
  def created(self) -> datetime:
571
577
  return self.publishable_entity_version.created
572
578
 
579
+ @property
580
+ def created_by(self):
581
+ return self.publishable_entity_version.created_by
582
+
573
583
  @property
574
584
  def version_num(self) -> int:
575
585
  return self.publishable_entity_version.version_num
576
586
 
587
+ def __str__(self) -> str:
588
+ return str(self.publishable_entity_version)
589
+
577
590
  class Meta:
578
591
  abstract = True
579
592
 
@@ -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
 
@@ -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"
@@ -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
 
@@ -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
 
@@ -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.0
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -19,12 +19,13 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Requires-Python: >=3.11
21
21
  License-File: LICENSE.txt
22
- Requires-Dist: djangorestframework<4.0
23
- Requires-Dist: celery
24
- Requires-Dist: edx-drf-extensions
25
- Requires-Dist: attrs
26
22
  Requires-Dist: Django
27
23
  Requires-Dist: rules<4.0
24
+ Requires-Dist: celery
25
+ Requires-Dist: djangorestframework<4.0
26
+ Requires-Dist: attrs
27
+ Requires-Dist: tomlkit
28
+ Requires-Dist: edx-drf-extensions
28
29
  Dynamic: author
29
30
  Dynamic: author-email
30
31
  Dynamic: classifier
@@ -1,10 +1,20 @@
1
- openedx_learning/__init__.py,sha256=gxlH6AqH1wFwipo5FfaLp8R8Uxg7pp57boi7X8I4LT0,69
1
+ openedx_learning/__init__.py,sha256=SyGXwpGB_cwgn1A-xCULm9ZZQwV783LkH-64qx6sehg,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
4
  openedx_learning/api/authoring.py,sha256=1sh-hUH3pJLVIQHzzjlqWb_9uxf9y3-hanLpU4mRvXc,1061
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=T5hYOuelkK2-UnJNDObk2DnkoKPxKdc8LJ6MwtwaXRI,760
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=zjaeVJqZJhtITVaL2Pmopkg3bhgbSM8-t4J3IUeNHts,2496
14
+ openedx_learning/apps/authoring/backup_restore/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py,sha256=NiZ-fEDGgh9Q0IUjpjOIvm9BtzwnVp6LK6IY-OA_o98,1734
17
+ openedx_learning/apps/authoring/backup_restore/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
18
  openedx_learning/apps/authoring/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
19
  openedx_learning/apps/authoring/collections/admin.py,sha256=f0hySjDMfIphVDEGkCSMIUHoEiqHRA7EE2NiO7lvL4g,1156
10
20
  openedx_learning/apps/authoring/collections/api.py,sha256=DaGg73iom7fN9fODajo8B2e9Jkx2syfLEVjip0cAzlQ,7747
@@ -36,7 +46,7 @@ openedx_learning/apps/authoring/contents/models.py,sha256=8CYrHK7CZ4U3F7Den4ZV_a
36
46
  openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTmIGX2KHpjw-PHbfRjxkFEomI5CEDhNKCZ7IpFeE,3060
37
47
  openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
48
  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
49
+ openedx_learning/apps/authoring/publishing/admin.py,sha256=nvAAl3Xswqqq3WyaI1NT7pLCcu1o-ynciJZOlc-9L24,16244
40
50
  openedx_learning/apps/authoring/publishing/api.py,sha256=BQ5Q0JsSRBmb8ZRKzUlEOOV6QxNUjYkkqCjxmnX9O48,55565
41
51
  openedx_learning/apps/authoring/publishing/apps.py,sha256=v9PTe3YoICaYT9wfu268ZkVAlnZFvxi-DqYdbRi25bY,750
42
52
  openedx_learning/apps/authoring/publishing/contextmanagers.py,sha256=AH5zhr0Tz_gUG9--dfr_oZAu8DMy94n6mnOJuPbWkeU,6723
@@ -55,21 +65,24 @@ openedx_learning/apps/authoring/publishing/models/draft_log.py,sha256=7hpbtnnc3y
55
65
  openedx_learning/apps/authoring/publishing/models/entity_list.py,sha256=8MyJqDdC8dqJY-N9UAu-WS-ZeFOYuRNMKloSY9wMH1w,3042
56
66
  openedx_learning/apps/authoring/publishing/models/learning_package.py,sha256=1fuNLHD6k0qGuL0jXYGf4-TA5WczgxJrXUdAIM_JNBI,2688
57
67
  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
68
+ openedx_learning/apps/authoring/publishing/models/publishable_entity.py,sha256=ncks-eNn5h-b1FjWPMAYtnuiMSvFYHwsGeNWGJA8GBE,25773
59
69
  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
70
+ openedx_learning/apps/authoring/sections/admin.py,sha256=OQOTtXYM-Zj8BBb1wNBkOxgkF2Pv3JdUrZ45VOEmThM,1757
71
+ openedx_learning/apps/authoring/sections/api.py,sha256=qfT29Vgcc-1d3-BTiKY_a5M9H7oFBXVD8KqWe-1OqM0,10323
72
+ openedx_learning/apps/authoring/sections/apps.py,sha256=h2-1csh-f3tLkauxXvn1xvi_P-l-_oe819XYO6BIYm4,738
62
73
  openedx_learning/apps/authoring/sections/models.py,sha256=2GK-dDMJwNRw_9gNFho8iKcDV-iYz_zBzqGMDmQ_jbc,1450
63
74
  openedx_learning/apps/authoring/sections/migrations/0001_initial.py,sha256=iW5AFhC26mfZNWEVNu8cTsr32Ca4htL4CUanHoXfaeY,1152
64
75
  openedx_learning/apps/authoring/sections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
76
  openedx_learning/apps/authoring/subsections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- openedx_learning/apps/authoring/subsections/api.py,sha256=Cgjq3Y6ZZTzIQXMX6p7Y4jhFwIBFwKhSY_HCTeziMGc,10260
77
+ openedx_learning/apps/authoring/subsections/admin.py,sha256=vPfOeTzh10aRhtZjXAzYsbwfw4Hc5yuySbpjAvtDH98,1795
78
+ openedx_learning/apps/authoring/subsections/api.py,sha256=7heZHREJy4t0L8b6mOLtpi0JxLjqsJ7sHQFZc8z07a4,10229
67
79
  openedx_learning/apps/authoring/subsections/apps.py,sha256=WueCaPOE-7x3cu-6rA9FdeKzipCZSNIhvqpAbxTysOg,773
68
80
  openedx_learning/apps/authoring/subsections/models.py,sha256=1uhdpS9Eg6keSqkzQaE8-XSVLAQlmi0llIIU2V7Nl44,1492
69
81
  openedx_learning/apps/authoring/subsections/migrations/0001_initial.py,sha256=7kEHIC-EwG2KvlW4hg5tnl45--dW4Yv5gqV5SDqNYNo,1158
70
82
  openedx_learning/apps/authoring/subsections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
83
  openedx_learning/apps/authoring/units/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- openedx_learning/apps/authoring/units/api.py,sha256=KJA7Bh7IBE22E6cMSZNp6pamjjA1Vf8EhHw0rvQtlPM,9797
84
+ openedx_learning/apps/authoring/units/admin.py,sha256=chp-bTfufBiQ3uycVF1DBEFSPvwXaROJnyyY8AaH_yw,1717
85
+ openedx_learning/apps/authoring/units/api.py,sha256=Bk6CndTU3sO0SvAphEl5UWKGBkWD_TE9p12BXBxxvLQ,9767
73
86
  openedx_learning/apps/authoring/units/apps.py,sha256=cIzphjDw5sjIZ3NLE911N7IMUa8JQSXMReNl03uI7jg,701
74
87
  openedx_learning/apps/authoring/units/models.py,sha256=eTOwFWC9coQLf0ovx08Mj7zi8mPAWCw9QOznybajRk0,1418
75
88
  openedx_learning/apps/authoring/units/migrations/0001_initial.py,sha256=qM_0JGffxECVgXzncHXfgSE-g8u3L3a14R0M1Bnj_Ys,1129
@@ -80,14 +93,14 @@ openedx_learning/contrib/media_server/apps.py,sha256=GicFBN3N6wzVs5i3RgrQFJZeMlS
80
93
  openedx_learning/contrib/media_server/urls.py,sha256=newNjV41sM9A9Oy_rgnZSXdkTFxSHiupIiAsVIGE2CE,365
81
94
  openedx_learning/contrib/media_server/views.py,sha256=qZPhdEW_oYj1MEdgLVP6Cq3tRiZtp7dTb7ASaSKZ2HY,1350
82
95
  openedx_learning/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- openedx_learning/lib/admin_utils.py,sha256=5z9NrXxmT5j8azx9u1t0AgxV5PIDTc2jPyM5z5yW8cw,4021
96
+ openedx_learning/lib/admin_utils.py,sha256=dQ02NrgXIJL6kx9LFBgEYc1Pr_xtFdr7NVXx01VOIE0,4514
84
97
  openedx_learning/lib/cache.py,sha256=ppT36KiPLdsAF3GfZCF0IdiHodckd2gLiF1sNhjSJuk,958
85
98
  openedx_learning/lib/collations.py,sha256=f65575r3BfAvFWU6pdBMsqrxPwFijB2SbJtDXq4UVc4,2401
86
99
  openedx_learning/lib/fields.py,sha256=eiGoXMPhRuq25EH2qf6BAODshAQE3DBVdIYAMIUAXW0,7522
87
100
  openedx_learning/lib/managers.py,sha256=-Q3gxalSqyPZ9Im4DTROW5tF8wVTZLlmfTe62_xmowY,1643
88
101
  openedx_learning/lib/test_utils.py,sha256=g3KLuepIZbaDBCsaj9711YuqyUx7LD4gXDcfNC-mWdc,527
89
102
  openedx_learning/lib/validators.py,sha256=iqEdEAvFV2tC7Ecssx69kjecpdU8nE87AlDJYrqrsnc,404
90
- openedx_learning-0.26.0.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
103
+ openedx_learning-0.27.0.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
91
104
  openedx_tagging/__init__.py,sha256=V9N8M7f9LYlAbA_DdPUsHzTnWjYRXKGa5qHw9P1JnNI,30
92
105
  openedx_tagging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
106
  openedx_tagging/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -129,7 +142,7 @@ openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py,sha25
129
142
  openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py,sha256=zmr4b65T0vX6fYc8MpvSmQnYkAiNMpx3RKEd5tudsl8,517
130
143
  openedx_tagging/core/tagging/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
144
  openedx_tagging/core/tagging/models/__init__.py,sha256=yYdOnthuc7EUdfEULtZgqRwn5Y4bbYQmJCjVZqR5GTM,236
132
- openedx_tagging/core/tagging/models/base.py,sha256=RXv4jDncV-JPXMAb62dGYNJhMdiS6kSo24eoG7UeKR4,39655
145
+ openedx_tagging/core/tagging/models/base.py,sha256=ju4mvgRS_I2AgPsRf4sMFy6qle2i0aA0MbyBYZXf32g,39685
133
146
  openedx_tagging/core/tagging/models/import_export.py,sha256=Aj0pleh0nh2LNS6zmdB1P4bpdgUMmvmobTkqBerORAI,4570
134
147
  openedx_tagging/core/tagging/models/system_defined.py,sha256=_6LfvUZGEltvQMtm2OXy6TOLh3C8GnVTqtZDSAZW6K4,9062
135
148
  openedx_tagging/core/tagging/models/utils.py,sha256=-A3Dj24twmTf65UB7G4WLvb_9qEvduEPIwahZ-FJDlg,1926
@@ -143,7 +156,7 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=0HQD_Jrf6-YpocYfz
143
156
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
144
157
  openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=Hf92cy-tE767DE9FgsZcPKiCYrf5ihfETz8qGKBnuiU,36278
145
158
  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,,
159
+ openedx_learning-0.27.0.dist-info/METADATA,sha256=aj1eSVrsZiti6xVUZKZONiQbNPv-5tD8AbAkiOKbgvk,9055
160
+ openedx_learning-0.27.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
161
+ openedx_learning-0.27.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
162
+ openedx_learning-0.27.0.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,