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.
- openedx_learning/__init__.py +1 -1
- openedx_learning/api/authoring.py +1 -0
- openedx_learning/apps/authoring/backup_restore/__init__.py +0 -0
- openedx_learning/apps/authoring/backup_restore/admin.py +3 -0
- openedx_learning/apps/authoring/backup_restore/api.py +15 -0
- openedx_learning/apps/authoring/backup_restore/apps.py +12 -0
- openedx_learning/apps/authoring/backup_restore/management/__init__.py +0 -0
- openedx_learning/apps/authoring/backup_restore/management/commands/__init__.py +0 -0
- openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +44 -0
- openedx_learning/apps/authoring/backup_restore/migrations/__init__.py +0 -0
- openedx_learning/apps/authoring/backup_restore/models.py +3 -0
- openedx_learning/apps/authoring/backup_restore/toml.py +75 -0
- openedx_learning/apps/authoring/backup_restore/zipper.py +53 -0
- openedx_learning/apps/authoring/components/apps.py +2 -2
- openedx_learning/apps/authoring/publishing/admin.py +277 -12
- openedx_learning/apps/authoring/publishing/api.py +41 -8
- openedx_learning/apps/authoring/publishing/apps.py +2 -2
- openedx_learning/apps/authoring/publishing/models/publishable_entity.py +20 -3
- openedx_learning/apps/authoring/sections/admin.py +48 -0
- openedx_learning/apps/authoring/sections/api.py +6 -2
- openedx_learning/apps/authoring/sections/apps.py +4 -4
- openedx_learning/apps/authoring/subsections/admin.py +48 -0
- openedx_learning/apps/authoring/subsections/api.py +6 -2
- openedx_learning/apps/authoring/subsections/apps.py +2 -2
- openedx_learning/apps/authoring/units/admin.py +48 -0
- openedx_learning/apps/authoring/units/api.py +6 -2
- openedx_learning/apps/authoring/units/apps.py +2 -2
- openedx_learning/lib/admin_utils.py +17 -1
- {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/METADATA +4 -3
- {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/RECORD +34 -20
- {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/WHEEL +1 -1
- openedx_tagging/core/tagging/models/base.py +1 -1
- {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/licenses/LICENSE.txt +0 -0
- {openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -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 *
|
|
File without changes
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
816
|
+
from ..publishing.api import register_publishable_models
|
|
809
817
|
from .models import Component, ComponentVersion
|
|
810
818
|
|
|
811
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
``
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
40
|
-
openedx_learning/apps/authoring/publishing/api.py,sha256=
|
|
41
|
-
openedx_learning/apps/authoring/publishing/apps.py,sha256=
|
|
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
|
|
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/
|
|
61
|
-
openedx_learning/apps/authoring/sections/
|
|
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/
|
|
67
|
-
openedx_learning/apps/authoring/subsections/
|
|
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/
|
|
73
|
-
openedx_learning/apps/authoring/units/
|
|
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=
|
|
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.
|
|
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=
|
|
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.
|
|
147
|
-
openedx_learning-0.
|
|
148
|
-
openedx_learning-0.
|
|
149
|
-
openedx_learning-0.
|
|
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,,
|
|
@@ -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,
|
{openedx_learning-0.26.0.dist-info → openedx_learning-0.27.1.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|