openedx-core 0.34.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_content/__init__.py +0 -0
- openedx_content/admin.py +13 -0
- openedx_content/api.py +20 -0
- openedx_content/applets/__init__.py +0 -0
- openedx_content/applets/backup_restore/__init__.py +0 -0
- openedx_content/applets/backup_restore/admin.py +3 -0
- openedx_content/applets/backup_restore/api.py +30 -0
- openedx_content/applets/backup_restore/models.py +3 -0
- openedx_content/applets/backup_restore/serializers.py +165 -0
- openedx_content/applets/backup_restore/toml.py +254 -0
- openedx_content/applets/backup_restore/zipper.py +1091 -0
- openedx_content/applets/collections/__init__.py +0 -0
- openedx_content/applets/collections/admin.py +42 -0
- openedx_content/applets/collections/api.py +251 -0
- openedx_content/applets/collections/models.py +238 -0
- openedx_content/applets/components/__init__.py +0 -0
- openedx_content/applets/components/admin.py +159 -0
- openedx_content/applets/components/api.py +659 -0
- openedx_content/applets/components/models.py +271 -0
- openedx_content/applets/contents/__init__.py +0 -0
- openedx_content/applets/contents/admin.py +71 -0
- openedx_content/applets/contents/api.py +240 -0
- openedx_content/applets/contents/models.py +417 -0
- openedx_content/applets/publishing/__init__.py +0 -0
- openedx_content/applets/publishing/admin.py +611 -0
- openedx_content/applets/publishing/api.py +2020 -0
- openedx_content/applets/publishing/contextmanagers.py +157 -0
- openedx_content/applets/publishing/models/__init__.py +28 -0
- openedx_content/applets/publishing/models/container.py +70 -0
- openedx_content/applets/publishing/models/draft_log.py +391 -0
- openedx_content/applets/publishing/models/entity_list.py +90 -0
- openedx_content/applets/publishing/models/learning_package.py +75 -0
- openedx_content/applets/publishing/models/publish_log.py +236 -0
- openedx_content/applets/publishing/models/publishable_entity.py +685 -0
- openedx_content/applets/sections/__init__.py +0 -0
- openedx_content/applets/sections/admin.py +48 -0
- openedx_content/applets/sections/api.py +330 -0
- openedx_content/applets/sections/models.py +50 -0
- openedx_content/applets/subsections/__init__.py +0 -0
- openedx_content/applets/subsections/admin.py +48 -0
- openedx_content/applets/subsections/api.py +329 -0
- openedx_content/applets/subsections/models.py +50 -0
- openedx_content/applets/units/__init__.py +0 -0
- openedx_content/applets/units/admin.py +48 -0
- openedx_content/applets/units/api.py +326 -0
- openedx_content/applets/units/models.py +50 -0
- openedx_content/apps.py +52 -0
- openedx_content/backcompat/__init__.py +0 -0
- openedx_content/backcompat/backup_restore/__init__.py +0 -0
- openedx_content/backcompat/backup_restore/apps.py +12 -0
- openedx_content/backcompat/backup_restore/migrations/__init__.py +0 -0
- openedx_content/backcompat/collections/__init__.py +0 -0
- openedx_content/backcompat/collections/apps.py +14 -0
- openedx_content/backcompat/collections/migrations/0001_initial.py +33 -0
- openedx_content/backcompat/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +53 -0
- openedx_content/backcompat/collections/migrations/0003_collection_entities.py +38 -0
- openedx_content/backcompat/collections/migrations/0004_collection_key.py +57 -0
- openedx_content/backcompat/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +18 -0
- openedx_content/backcompat/collections/migrations/0006_remove_all_field_state_for_move_to_applet.py +82 -0
- openedx_content/backcompat/collections/migrations/__init__.py +0 -0
- openedx_content/backcompat/collections/models.py +9 -0
- openedx_content/backcompat/components/__init__.py +0 -0
- openedx_content/backcompat/components/apps.py +14 -0
- openedx_content/backcompat/components/migrations/0001_initial.py +97 -0
- openedx_content/backcompat/components/migrations/0002_alter_componentversioncontent_key.py +20 -0
- openedx_content/backcompat/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +17 -0
- openedx_content/backcompat/components/migrations/0004_remove_componentversioncontent_uuid.py +17 -0
- openedx_content/backcompat/components/migrations/0005_remove_all_field_state_for_move_to_applet.py +72 -0
- openedx_content/backcompat/components/migrations/__init__.py +0 -0
- openedx_content/backcompat/components/models.py +11 -0
- openedx_content/backcompat/contents/__init__.py +0 -0
- openedx_content/backcompat/contents/apps.py +14 -0
- openedx_content/backcompat/contents/migrations/0001_initial.py +66 -0
- openedx_content/backcompat/contents/migrations/0002_remove_all_field_state_for_move_to_applet.py +26 -0
- openedx_content/backcompat/contents/migrations/__init__.py +0 -0
- openedx_content/backcompat/publishing/__init__.py +0 -0
- openedx_content/backcompat/publishing/apps.py +14 -0
- openedx_content/backcompat/publishing/migrations/0001_initial.py +166 -0
- openedx_content/backcompat/publishing/migrations/0002_alter_learningpackage_key_and_more.py +25 -0
- openedx_content/backcompat/publishing/migrations/0003_containers.py +54 -0
- openedx_content/backcompat/publishing/migrations/0004_publishableentity_can_stand_alone.py +21 -0
- openedx_content/backcompat/publishing/migrations/0005_alter_entitylistrow_options.py +17 -0
- openedx_content/backcompat/publishing/migrations/0006_draftchangelog.py +68 -0
- openedx_content/backcompat/publishing/migrations/0007_bootstrap_draftchangelog.py +94 -0
- openedx_content/backcompat/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +31 -0
- openedx_content/backcompat/publishing/migrations/0009_dependencies_and_hashing.py +62 -0
- openedx_content/backcompat/publishing/migrations/0010_backfill_dependencies.py +432 -0
- openedx_content/backcompat/publishing/migrations/0011_remove_all_field_state_for_move_to_applet.py +288 -0
- openedx_content/backcompat/publishing/migrations/__init__.py +0 -0
- openedx_content/backcompat/publishing/models.py +27 -0
- openedx_content/backcompat/sections/__init__.py +0 -0
- openedx_content/backcompat/sections/apps.py +15 -0
- openedx_content/backcompat/sections/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/sections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/sections/migrations/__init__.py +0 -0
- openedx_content/backcompat/subsections/__init__.py +0 -0
- openedx_content/backcompat/subsections/apps.py +15 -0
- openedx_content/backcompat/subsections/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/subsections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/subsections/migrations/__init__.py +0 -0
- openedx_content/backcompat/units/__init__.py +0 -0
- openedx_content/backcompat/units/apps.py +15 -0
- openedx_content/backcompat/units/migrations/0001_initial.py +36 -0
- openedx_content/backcompat/units/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
- openedx_content/backcompat/units/migrations/__init__.py +0 -0
- openedx_content/management/__init__.py +0 -0
- openedx_content/management/commands/__init__.py +0 -0
- openedx_content/management/commands/add_assets_to_component.py +86 -0
- openedx_content/management/commands/lp_dump.py +69 -0
- openedx_content/management/commands/lp_load.py +57 -0
- openedx_content/migrations/0001_initial.py +654 -0
- openedx_content/migrations/0002_rename_tables_to_openedx_content.py +138 -0
- openedx_content/migrations/__init__.py +0 -0
- openedx_content/models.py +17 -0
- openedx_content/models_api.py +16 -0
- openedx_content/settings_api.py +38 -0
- openedx_core/__init__.py +9 -0
- openedx_core-0.34.0.dist-info/METADATA +152 -0
- openedx_core-0.34.0.dist-info/RECORD +181 -0
- openedx_core-0.34.0.dist-info/WHEEL +6 -0
- openedx_core-0.34.0.dist-info/licenses/LICENSE.txt +674 -0
- openedx_core-0.34.0.dist-info/top_level.txt +4 -0
- openedx_django_lib/__init__.py +6 -0
- openedx_django_lib/admin_utils.py +116 -0
- openedx_django_lib/collations.py +65 -0
- openedx_django_lib/fields.py +208 -0
- openedx_django_lib/managers.py +48 -0
- openedx_django_lib/validators.py +15 -0
- openedx_tagging/__init__.py +0 -0
- openedx_tagging/admin.py +44 -0
- openedx_tagging/api.py +525 -0
- openedx_tagging/apps.py +19 -0
- openedx_tagging/data.py +40 -0
- openedx_tagging/import_export/__init__.py +4 -0
- openedx_tagging/import_export/actions.py +452 -0
- openedx_tagging/import_export/api.py +232 -0
- openedx_tagging/import_export/exceptions.py +113 -0
- openedx_tagging/import_export/import_plan.py +218 -0
- openedx_tagging/import_export/parsers.py +326 -0
- openedx_tagging/import_export/tasks.py +43 -0
- openedx_tagging/import_export/template.csv +30 -0
- openedx_tagging/import_export/template.json +158 -0
- openedx_tagging/migrations/0001_initial.py +212 -0
- openedx_tagging/migrations/0001_squashed.py +152 -0
- openedx_tagging/migrations/0002_auto_20230718_2026.py +79 -0
- openedx_tagging/migrations/0003_auto_20230721_1238.py +76 -0
- openedx_tagging/migrations/0004_auto_20230723_2001.py +35 -0
- openedx_tagging/migrations/0005_language_taxonomy.py +31 -0
- openedx_tagging/migrations/0006_alter_objecttag_unique_together.py +16 -0
- openedx_tagging/migrations/0006_auto_20230802_1631.py +82 -0
- openedx_tagging/migrations/0007_tag_import_task_log_null_fix.py +19 -0
- openedx_tagging/migrations/0008_taxonomy_description_not_null.py +24 -0
- openedx_tagging/migrations/0009_alter_objecttag_object_id.py +20 -0
- openedx_tagging/migrations/0010_cleanups.py +32 -0
- openedx_tagging/migrations/0011_remove_required.py +22 -0
- openedx_tagging/migrations/0012_language_taxonomy.py +43 -0
- openedx_tagging/migrations/0013_tag_parent_blank.py +19 -0
- openedx_tagging/migrations/0014_minor_fixes.py +36 -0
- openedx_tagging/migrations/0015_taxonomy_export_id.py +38 -0
- openedx_tagging/migrations/0016_object_tag_export_id.py +62 -0
- openedx_tagging/migrations/0017_alter_tagimporttask_status.py +18 -0
- openedx_tagging/migrations/0018_objecttag_is_copied.py +18 -0
- openedx_tagging/migrations/__init__.py +0 -0
- openedx_tagging/models/__init__.py +6 -0
- openedx_tagging/models/base.py +996 -0
- openedx_tagging/models/import_export.py +150 -0
- openedx_tagging/models/system_defined.py +248 -0
- openedx_tagging/models/utils.py +82 -0
- openedx_tagging/py.typed +0 -0
- openedx_tagging/rest_api/__init__.py +0 -0
- openedx_tagging/rest_api/paginators.py +89 -0
- openedx_tagging/rest_api/urls.py +9 -0
- openedx_tagging/rest_api/utils.py +109 -0
- openedx_tagging/rest_api/v1/__init__.py +0 -0
- openedx_tagging/rest_api/v1/permissions.py +68 -0
- openedx_tagging/rest_api/v1/serializers.py +387 -0
- openedx_tagging/rest_api/v1/urls.py +27 -0
- openedx_tagging/rest_api/v1/views.py +935 -0
- openedx_tagging/rest_api/v1/views_import.py +51 -0
- openedx_tagging/rules.py +206 -0
- openedx_tagging/urls.py +10 -0
|
File without changes
|
openedx_content/admin.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module aggregates all applet Django Admin modules.
|
|
3
|
+
"""
|
|
4
|
+
# pylint: disable=wildcard-import
|
|
5
|
+
|
|
6
|
+
from .applets.backup_restore.admin import *
|
|
7
|
+
from .applets.collections.admin import *
|
|
8
|
+
from .applets.components.admin import *
|
|
9
|
+
from .applets.contents.admin import *
|
|
10
|
+
from .applets.publishing.admin import *
|
|
11
|
+
from .applets.sections.admin import *
|
|
12
|
+
from .applets.subsections.admin import *
|
|
13
|
+
from .applets.units.admin import *
|
openedx_content/api.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the public API for content authoring in the Open edX Core.
|
|
3
|
+
|
|
4
|
+
This is the single ``api`` module that code outside of the
|
|
5
|
+
``openedx_content.*`` package should import from. It will
|
|
6
|
+
re-export the public functions from all api.py modules of its applets.
|
|
7
|
+
It may also implement its own convenience APIs that wrap calls to multiple app
|
|
8
|
+
APIs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# These wildcard imports are okay because these api modules declare __all__.
|
|
12
|
+
# pylint: disable=wildcard-import
|
|
13
|
+
from .applets.backup_restore.api import *
|
|
14
|
+
from .applets.collections.api import *
|
|
15
|
+
from .applets.components.api import *
|
|
16
|
+
from .applets.contents.api import *
|
|
17
|
+
from .applets.publishing.api import *
|
|
18
|
+
from .applets.sections.api import *
|
|
19
|
+
from .applets.subsections.api import *
|
|
20
|
+
from .applets.units.api import *
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backup Restore API
|
|
3
|
+
"""
|
|
4
|
+
import zipfile
|
|
5
|
+
|
|
6
|
+
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
|
|
7
|
+
|
|
8
|
+
from ..publishing.api import get_learning_package_by_key
|
|
9
|
+
from .zipper import LearningPackageUnzipper, LearningPackageZipper
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_zip_file(lp_key: str, path: str, user: UserType | None = None, origin_server: str | None = None) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Creates a dump zip file for the given learning package key at the given path.
|
|
15
|
+
The zip file contains a TOML representation of the learning package and its contents.
|
|
16
|
+
|
|
17
|
+
Can throw a NotFoundError at get_learning_package_by_key
|
|
18
|
+
"""
|
|
19
|
+
learning_package = get_learning_package_by_key(lp_key)
|
|
20
|
+
LearningPackageZipper(learning_package, user, origin_server).create_zip(path)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_learning_package(path: str, key: str | None = None, user: UserType | None = None) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Loads a learning package from a zip file at the given path.
|
|
26
|
+
Restores the learning package and its contents to the database.
|
|
27
|
+
Returns a dictionary with the status of the operation and any errors encountered.
|
|
28
|
+
"""
|
|
29
|
+
with zipfile.ZipFile(path, "r") as zipf:
|
|
30
|
+
return LearningPackageUnzipper(zipf, key, user).load()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The serializers module for restoration of authoring data.
|
|
3
|
+
"""
|
|
4
|
+
from datetime import timezone
|
|
5
|
+
|
|
6
|
+
from rest_framework import serializers
|
|
7
|
+
|
|
8
|
+
from ..components import api as components_api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LearningPackageSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
12
|
+
"""
|
|
13
|
+
Serializer for learning packages.
|
|
14
|
+
|
|
15
|
+
Note:
|
|
16
|
+
The `key` field is serialized, but it is generally not trustworthy for restoration.
|
|
17
|
+
During restore, a new key may be generated or overridden.
|
|
18
|
+
"""
|
|
19
|
+
title = serializers.CharField(required=True)
|
|
20
|
+
key = serializers.CharField(required=True)
|
|
21
|
+
description = serializers.CharField(required=True, allow_blank=True)
|
|
22
|
+
created = serializers.DateTimeField(required=True, default_timezone=timezone.utc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LearningPackageMetadataSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
26
|
+
"""
|
|
27
|
+
Serializer for learning package metadata.
|
|
28
|
+
|
|
29
|
+
Note:
|
|
30
|
+
This serializer handles data exported to an archive (e.g., during backup),
|
|
31
|
+
but the metadata is not restored to the database and is meant solely for inspection.
|
|
32
|
+
"""
|
|
33
|
+
format_version = serializers.IntegerField(required=True)
|
|
34
|
+
created_by = serializers.CharField(required=False, allow_null=True)
|
|
35
|
+
created_by_email = serializers.EmailField(required=False, allow_null=True)
|
|
36
|
+
created_at = serializers.DateTimeField(required=True, default_timezone=timezone.utc)
|
|
37
|
+
origin_server = serializers.CharField(required=False, allow_null=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EntitySerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
41
|
+
"""
|
|
42
|
+
Serializer for publishable entities.
|
|
43
|
+
"""
|
|
44
|
+
can_stand_alone = serializers.BooleanField(required=True)
|
|
45
|
+
key = serializers.CharField(required=True)
|
|
46
|
+
created = serializers.DateTimeField(required=True, default_timezone=timezone.utc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EntityVersionSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
50
|
+
"""
|
|
51
|
+
Serializer for publishable entity versions.
|
|
52
|
+
"""
|
|
53
|
+
title = serializers.CharField(required=True)
|
|
54
|
+
entity_key = serializers.CharField(required=True)
|
|
55
|
+
created = serializers.DateTimeField(required=True, default_timezone=timezone.utc)
|
|
56
|
+
version_num = serializers.IntegerField(required=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ComponentSerializer(EntitySerializer): # pylint: disable=abstract-method
|
|
60
|
+
"""
|
|
61
|
+
Serializer for components.
|
|
62
|
+
Contains logic to convert entity_key to component_type and local_key.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def validate(self, attrs):
|
|
66
|
+
"""
|
|
67
|
+
Custom validation logic:
|
|
68
|
+
parse the entity_key into (component_type, local_key).
|
|
69
|
+
"""
|
|
70
|
+
entity_key = attrs["key"]
|
|
71
|
+
try:
|
|
72
|
+
component_type_obj, local_key = components_api.get_or_create_component_type_by_entity_key(entity_key)
|
|
73
|
+
attrs["component_type"] = component_type_obj
|
|
74
|
+
attrs["local_key"] = local_key
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
raise serializers.ValidationError({"key": str(exc)})
|
|
77
|
+
return attrs
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ComponentVersionSerializer(EntityVersionSerializer): # pylint: disable=abstract-method
|
|
81
|
+
"""
|
|
82
|
+
Serializer for component versions.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ContainerSerializer(EntitySerializer): # pylint: disable=abstract-method
|
|
87
|
+
"""
|
|
88
|
+
Serializer for containers.
|
|
89
|
+
"""
|
|
90
|
+
container = serializers.DictField(required=True)
|
|
91
|
+
|
|
92
|
+
def validate_container(self, value):
|
|
93
|
+
"""
|
|
94
|
+
Custom validation logic for the container field.
|
|
95
|
+
Ensures that the container dict has exactly one key which is one of
|
|
96
|
+
"section", "subsection", or "unit" values.
|
|
97
|
+
"""
|
|
98
|
+
errors = []
|
|
99
|
+
if not isinstance(value, dict) or len(value) != 1:
|
|
100
|
+
errors.append("Container must be a dict with exactly one key.")
|
|
101
|
+
if len(value) == 1: # Only check the key if there is exactly one
|
|
102
|
+
container_type = list(value.keys())[0]
|
|
103
|
+
if container_type not in ("section", "subsection", "unit"):
|
|
104
|
+
errors.append(f"Invalid container value: {container_type}")
|
|
105
|
+
if errors:
|
|
106
|
+
raise serializers.ValidationError(errors)
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def validate(self, attrs):
|
|
110
|
+
"""
|
|
111
|
+
Custom validation logic:
|
|
112
|
+
parse the container dict to extract the container type.
|
|
113
|
+
"""
|
|
114
|
+
container = attrs["container"]
|
|
115
|
+
container_type = list(container.keys())[0] # It is safe to do this after validate_container
|
|
116
|
+
attrs["container_type"] = container_type
|
|
117
|
+
attrs.pop("container") # Remove the container field after processing
|
|
118
|
+
return attrs
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ContainerVersionSerializer(EntityVersionSerializer): # pylint: disable=abstract-method
|
|
122
|
+
"""
|
|
123
|
+
Serializer for container versions.
|
|
124
|
+
"""
|
|
125
|
+
container = serializers.DictField(required=True)
|
|
126
|
+
|
|
127
|
+
def validate_container(self, value):
|
|
128
|
+
"""
|
|
129
|
+
Custom validation logic for the container field.
|
|
130
|
+
Ensures that the container dict has exactly one key "children" which is a list of strings.
|
|
131
|
+
"""
|
|
132
|
+
errors = []
|
|
133
|
+
if not isinstance(value, dict) or len(value) != 1:
|
|
134
|
+
errors.append("Container must be a dict with exactly one key.")
|
|
135
|
+
if "children" not in value:
|
|
136
|
+
errors.append("Container must have a 'children' key.")
|
|
137
|
+
if "children" in value and not isinstance(value["children"], list):
|
|
138
|
+
errors.append("'children' must be a list.")
|
|
139
|
+
if errors:
|
|
140
|
+
raise serializers.ValidationError(errors)
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
def validate(self, attrs):
|
|
144
|
+
"""
|
|
145
|
+
Custom validation logic:
|
|
146
|
+
parse the container dict to extract the children list.
|
|
147
|
+
"""
|
|
148
|
+
children = attrs["container"]["children"] # It is safe to do this after validate_container
|
|
149
|
+
attrs["children"] = children
|
|
150
|
+
attrs.pop("container") # Remove the container field after processing
|
|
151
|
+
return attrs
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CollectionSerializer(serializers.Serializer): # pylint: disable=abstract-method
|
|
155
|
+
"""
|
|
156
|
+
Serializer for collections.
|
|
157
|
+
"""
|
|
158
|
+
title = serializers.CharField(required=True)
|
|
159
|
+
key = serializers.CharField(required=True)
|
|
160
|
+
description = serializers.CharField(required=True, allow_blank=True)
|
|
161
|
+
entities = serializers.ListField(
|
|
162
|
+
child=serializers.CharField(),
|
|
163
|
+
required=True,
|
|
164
|
+
allow_empty=True,
|
|
165
|
+
)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TOML serialization for learning packages and publishable entities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
import tomlkit
|
|
9
|
+
from django.contrib.auth.models import User as UserType # pylint: disable=imported-auth-user
|
|
10
|
+
|
|
11
|
+
from ..collections.models import Collection
|
|
12
|
+
from ..publishing import api as publishing_api
|
|
13
|
+
from ..publishing.models import PublishableEntity, PublishableEntityVersion
|
|
14
|
+
from ..publishing.models.learning_package import LearningPackage
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def toml_learning_package(
|
|
18
|
+
learning_package: LearningPackage,
|
|
19
|
+
timestamp: datetime,
|
|
20
|
+
format_version: int = 1,
|
|
21
|
+
user: UserType | None = None,
|
|
22
|
+
origin_server: str | None = None
|
|
23
|
+
) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Create a TOML representation of the learning package.
|
|
26
|
+
|
|
27
|
+
The resulting content looks like:
|
|
28
|
+
[meta]
|
|
29
|
+
format_version = 1
|
|
30
|
+
created_by = "dormsbee"
|
|
31
|
+
created_at = 2025-09-03T17:50:59.536190Z
|
|
32
|
+
origin_server = "cms.test"
|
|
33
|
+
|
|
34
|
+
[learning_package]
|
|
35
|
+
title = "Components Test Case Learning Package"
|
|
36
|
+
key = "ComponentTestCase-test-key"
|
|
37
|
+
description = "This is a test learning package for components."
|
|
38
|
+
created = 2025-09-03T17:50:59.536190Z
|
|
39
|
+
updated = 2025-09-03T17:50:59.536190Z
|
|
40
|
+
"""
|
|
41
|
+
doc = tomlkit.document()
|
|
42
|
+
|
|
43
|
+
# Learning package main info
|
|
44
|
+
section = tomlkit.table()
|
|
45
|
+
section.add("title", learning_package.title)
|
|
46
|
+
section.add("key", learning_package.key)
|
|
47
|
+
section.add("description", learning_package.description)
|
|
48
|
+
section.add("created", learning_package.created)
|
|
49
|
+
section.add("updated", learning_package.updated)
|
|
50
|
+
|
|
51
|
+
# Learning package metadata
|
|
52
|
+
metadata = tomlkit.table()
|
|
53
|
+
metadata.add("format_version", format_version)
|
|
54
|
+
if user:
|
|
55
|
+
metadata.add("created_by", user.username)
|
|
56
|
+
metadata.add("created_by_email", user.email)
|
|
57
|
+
metadata.add("created_at", timestamp)
|
|
58
|
+
if origin_server:
|
|
59
|
+
metadata.add("origin_server", origin_server)
|
|
60
|
+
|
|
61
|
+
doc.add("meta", metadata)
|
|
62
|
+
doc.add("learning_package", section)
|
|
63
|
+
return tomlkit.dumps(doc)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_toml_publishable_entity_table(
|
|
67
|
+
entity: PublishableEntity,
|
|
68
|
+
draft_version: PublishableEntityVersion | None,
|
|
69
|
+
published_version: PublishableEntityVersion | None,
|
|
70
|
+
include_versions: bool = True) -> tomlkit.items.Table:
|
|
71
|
+
"""
|
|
72
|
+
Create a TOML representation of a publishable entity.
|
|
73
|
+
|
|
74
|
+
The resulting content looks like:
|
|
75
|
+
[entity]
|
|
76
|
+
can_stand_alone = true
|
|
77
|
+
key = "xblock.v1:problem:my_published_example"
|
|
78
|
+
|
|
79
|
+
[entity.draft]
|
|
80
|
+
version_num = 2
|
|
81
|
+
|
|
82
|
+
[entity.published]
|
|
83
|
+
version_num = 1
|
|
84
|
+
|
|
85
|
+
[entity.container.section]
|
|
86
|
+
|
|
87
|
+
Note: This function returns a tomlkit.items.Table, which represents
|
|
88
|
+
a string-like TOML fragment rather than a complete TOML document.
|
|
89
|
+
"""
|
|
90
|
+
entity_table = tomlkit.table()
|
|
91
|
+
entity_table.add("can_stand_alone", entity.can_stand_alone)
|
|
92
|
+
# Add key since the toml filename doesn't show the real key
|
|
93
|
+
entity_table.add("key", entity.key)
|
|
94
|
+
entity_table.add("created", entity.created)
|
|
95
|
+
|
|
96
|
+
if not include_versions:
|
|
97
|
+
return entity_table
|
|
98
|
+
|
|
99
|
+
if draft_version:
|
|
100
|
+
draft_table = tomlkit.table()
|
|
101
|
+
draft_table.add("version_num", draft_version.version_num)
|
|
102
|
+
entity_table.add("draft", draft_table)
|
|
103
|
+
|
|
104
|
+
published_table = tomlkit.table()
|
|
105
|
+
if published_version:
|
|
106
|
+
published_table.add("version_num", published_version.version_num)
|
|
107
|
+
else:
|
|
108
|
+
published_table.add(tomlkit.comment("unpublished: no published_version_num"))
|
|
109
|
+
entity_table.add("published", published_table)
|
|
110
|
+
|
|
111
|
+
if hasattr(entity, "container"):
|
|
112
|
+
container_table = tomlkit.table()
|
|
113
|
+
container_types = ["section", "subsection", "unit"]
|
|
114
|
+
|
|
115
|
+
for container_type in container_types:
|
|
116
|
+
if hasattr(entity.container, container_type):
|
|
117
|
+
container_table.add(container_type, tomlkit.table())
|
|
118
|
+
break # stop after the first match
|
|
119
|
+
|
|
120
|
+
entity_table.add("container", container_table)
|
|
121
|
+
|
|
122
|
+
return entity_table
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def toml_publishable_entity(
|
|
126
|
+
entity: PublishableEntity,
|
|
127
|
+
versions_to_write: list[PublishableEntityVersion],
|
|
128
|
+
draft_version: PublishableEntityVersion | None,
|
|
129
|
+
published_version: PublishableEntityVersion | None) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Create a TOML representation of a publishable entity and its versions.
|
|
132
|
+
|
|
133
|
+
The resulting content looks like:
|
|
134
|
+
[entity]
|
|
135
|
+
can_stand_alone = true
|
|
136
|
+
key = "xblock.v1:problem:my_published_example"
|
|
137
|
+
|
|
138
|
+
[entity.draft]
|
|
139
|
+
version_num = 2
|
|
140
|
+
|
|
141
|
+
[entity.published]
|
|
142
|
+
version_num = 1
|
|
143
|
+
|
|
144
|
+
[entity.container.section] (if applicable)
|
|
145
|
+
|
|
146
|
+
# ### Versions
|
|
147
|
+
|
|
148
|
+
[[version]]
|
|
149
|
+
title = "My published problem"
|
|
150
|
+
version_num = 1
|
|
151
|
+
|
|
152
|
+
[version.container] (if applicable)
|
|
153
|
+
children = []
|
|
154
|
+
"""
|
|
155
|
+
# Create the TOML representation for the entity itself
|
|
156
|
+
entity_table = _get_toml_publishable_entity_table(entity, draft_version, published_version)
|
|
157
|
+
doc = tomlkit.document()
|
|
158
|
+
doc.add("entity", entity_table)
|
|
159
|
+
|
|
160
|
+
# Add versions as an array of tables (AoT)
|
|
161
|
+
doc.add(tomlkit.nl())
|
|
162
|
+
doc.add(tomlkit.comment("### Versions"))
|
|
163
|
+
for entity_version in versions_to_write:
|
|
164
|
+
version = tomlkit.aot()
|
|
165
|
+
version_table = toml_publishable_entity_version(entity_version)
|
|
166
|
+
version.append(version_table)
|
|
167
|
+
doc.add("version", version)
|
|
168
|
+
|
|
169
|
+
return tomlkit.dumps(doc)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def toml_publishable_entity_version(version: PublishableEntityVersion) -> tomlkit.items.Table:
|
|
173
|
+
"""
|
|
174
|
+
Create a TOML representation of a publishable entity version.
|
|
175
|
+
|
|
176
|
+
The resulting content looks like:
|
|
177
|
+
[[version]]
|
|
178
|
+
title = "My published problem"
|
|
179
|
+
version_num = 1
|
|
180
|
+
|
|
181
|
+
[version.container] (if applicable)
|
|
182
|
+
children = []
|
|
183
|
+
|
|
184
|
+
Note: This function returns a tomlkit.items.Table, which represents
|
|
185
|
+
a string-like TOML fragment rather than a complete TOML document.
|
|
186
|
+
"""
|
|
187
|
+
version_table = tomlkit.table()
|
|
188
|
+
version_table.add("title", version.title)
|
|
189
|
+
version_table.add("version_num", version.version_num)
|
|
190
|
+
|
|
191
|
+
if hasattr(version, 'containerversion'):
|
|
192
|
+
# If the version has a container version, add its children
|
|
193
|
+
container_table = tomlkit.table()
|
|
194
|
+
children = publishing_api.get_container_children_entities_keys(version.containerversion)
|
|
195
|
+
container_table.add("children", children)
|
|
196
|
+
version_table.add("container", container_table)
|
|
197
|
+
return version_table
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def toml_collection(collection: Collection, entity_keys: list[str]) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Create a TOML representation of a collection.
|
|
203
|
+
|
|
204
|
+
The resulting content looks like:
|
|
205
|
+
[collection]
|
|
206
|
+
title = "Collection 1"
|
|
207
|
+
key = "COL1"
|
|
208
|
+
description = "Description of Collection 1"
|
|
209
|
+
created = 2025-09-03T22:28:53.839362Z
|
|
210
|
+
entities = [
|
|
211
|
+
"xblock.v1:problem:my_published_example",
|
|
212
|
+
"xblock.v1:html:my_draft_example",
|
|
213
|
+
]
|
|
214
|
+
"""
|
|
215
|
+
doc = tomlkit.document()
|
|
216
|
+
|
|
217
|
+
entities_array = tomlkit.array()
|
|
218
|
+
entities_array.extend(entity_keys)
|
|
219
|
+
entities_array.multiline(True)
|
|
220
|
+
|
|
221
|
+
collection_table = tomlkit.table()
|
|
222
|
+
collection_table.add("title", collection.title)
|
|
223
|
+
collection_table.add("key", collection.key)
|
|
224
|
+
collection_table.add("description", collection.description)
|
|
225
|
+
collection_table.add("created", collection.created)
|
|
226
|
+
collection_table.add("entities", entities_array)
|
|
227
|
+
|
|
228
|
+
doc.add("collection", collection_table)
|
|
229
|
+
|
|
230
|
+
return tomlkit.dumps(doc)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parse_learning_package_toml(content: str) -> dict:
|
|
234
|
+
"""
|
|
235
|
+
Parse the learning package TOML content and return a dict of its fields.
|
|
236
|
+
"""
|
|
237
|
+
lp_data: Dict[str, Any] = tomlkit.parse(content)
|
|
238
|
+
return lp_data
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def parse_publishable_entity_toml(content: str) -> dict:
|
|
242
|
+
"""
|
|
243
|
+
Parse the publishable entity TOML file and return a dict of its fields.
|
|
244
|
+
"""
|
|
245
|
+
pe_data: Dict[str, Any] = tomlkit.parse(content)
|
|
246
|
+
return pe_data
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def parse_collection_toml(content: str) -> dict:
|
|
250
|
+
"""
|
|
251
|
+
Parse the collection TOML content and return a dict of its fields.
|
|
252
|
+
"""
|
|
253
|
+
collection_data: Dict[str, Any] = tomlkit.parse(content)
|
|
254
|
+
return collection_data
|