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.
Files changed (181) hide show
  1. openedx_content/__init__.py +0 -0
  2. openedx_content/admin.py +13 -0
  3. openedx_content/api.py +20 -0
  4. openedx_content/applets/__init__.py +0 -0
  5. openedx_content/applets/backup_restore/__init__.py +0 -0
  6. openedx_content/applets/backup_restore/admin.py +3 -0
  7. openedx_content/applets/backup_restore/api.py +30 -0
  8. openedx_content/applets/backup_restore/models.py +3 -0
  9. openedx_content/applets/backup_restore/serializers.py +165 -0
  10. openedx_content/applets/backup_restore/toml.py +254 -0
  11. openedx_content/applets/backup_restore/zipper.py +1091 -0
  12. openedx_content/applets/collections/__init__.py +0 -0
  13. openedx_content/applets/collections/admin.py +42 -0
  14. openedx_content/applets/collections/api.py +251 -0
  15. openedx_content/applets/collections/models.py +238 -0
  16. openedx_content/applets/components/__init__.py +0 -0
  17. openedx_content/applets/components/admin.py +159 -0
  18. openedx_content/applets/components/api.py +659 -0
  19. openedx_content/applets/components/models.py +271 -0
  20. openedx_content/applets/contents/__init__.py +0 -0
  21. openedx_content/applets/contents/admin.py +71 -0
  22. openedx_content/applets/contents/api.py +240 -0
  23. openedx_content/applets/contents/models.py +417 -0
  24. openedx_content/applets/publishing/__init__.py +0 -0
  25. openedx_content/applets/publishing/admin.py +611 -0
  26. openedx_content/applets/publishing/api.py +2020 -0
  27. openedx_content/applets/publishing/contextmanagers.py +157 -0
  28. openedx_content/applets/publishing/models/__init__.py +28 -0
  29. openedx_content/applets/publishing/models/container.py +70 -0
  30. openedx_content/applets/publishing/models/draft_log.py +391 -0
  31. openedx_content/applets/publishing/models/entity_list.py +90 -0
  32. openedx_content/applets/publishing/models/learning_package.py +75 -0
  33. openedx_content/applets/publishing/models/publish_log.py +236 -0
  34. openedx_content/applets/publishing/models/publishable_entity.py +685 -0
  35. openedx_content/applets/sections/__init__.py +0 -0
  36. openedx_content/applets/sections/admin.py +48 -0
  37. openedx_content/applets/sections/api.py +330 -0
  38. openedx_content/applets/sections/models.py +50 -0
  39. openedx_content/applets/subsections/__init__.py +0 -0
  40. openedx_content/applets/subsections/admin.py +48 -0
  41. openedx_content/applets/subsections/api.py +329 -0
  42. openedx_content/applets/subsections/models.py +50 -0
  43. openedx_content/applets/units/__init__.py +0 -0
  44. openedx_content/applets/units/admin.py +48 -0
  45. openedx_content/applets/units/api.py +326 -0
  46. openedx_content/applets/units/models.py +50 -0
  47. openedx_content/apps.py +52 -0
  48. openedx_content/backcompat/__init__.py +0 -0
  49. openedx_content/backcompat/backup_restore/__init__.py +0 -0
  50. openedx_content/backcompat/backup_restore/apps.py +12 -0
  51. openedx_content/backcompat/backup_restore/migrations/__init__.py +0 -0
  52. openedx_content/backcompat/collections/__init__.py +0 -0
  53. openedx_content/backcompat/collections/apps.py +14 -0
  54. openedx_content/backcompat/collections/migrations/0001_initial.py +33 -0
  55. openedx_content/backcompat/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +53 -0
  56. openedx_content/backcompat/collections/migrations/0003_collection_entities.py +38 -0
  57. openedx_content/backcompat/collections/migrations/0004_collection_key.py +57 -0
  58. openedx_content/backcompat/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +18 -0
  59. openedx_content/backcompat/collections/migrations/0006_remove_all_field_state_for_move_to_applet.py +82 -0
  60. openedx_content/backcompat/collections/migrations/__init__.py +0 -0
  61. openedx_content/backcompat/collections/models.py +9 -0
  62. openedx_content/backcompat/components/__init__.py +0 -0
  63. openedx_content/backcompat/components/apps.py +14 -0
  64. openedx_content/backcompat/components/migrations/0001_initial.py +97 -0
  65. openedx_content/backcompat/components/migrations/0002_alter_componentversioncontent_key.py +20 -0
  66. openedx_content/backcompat/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +17 -0
  67. openedx_content/backcompat/components/migrations/0004_remove_componentversioncontent_uuid.py +17 -0
  68. openedx_content/backcompat/components/migrations/0005_remove_all_field_state_for_move_to_applet.py +72 -0
  69. openedx_content/backcompat/components/migrations/__init__.py +0 -0
  70. openedx_content/backcompat/components/models.py +11 -0
  71. openedx_content/backcompat/contents/__init__.py +0 -0
  72. openedx_content/backcompat/contents/apps.py +14 -0
  73. openedx_content/backcompat/contents/migrations/0001_initial.py +66 -0
  74. openedx_content/backcompat/contents/migrations/0002_remove_all_field_state_for_move_to_applet.py +26 -0
  75. openedx_content/backcompat/contents/migrations/__init__.py +0 -0
  76. openedx_content/backcompat/publishing/__init__.py +0 -0
  77. openedx_content/backcompat/publishing/apps.py +14 -0
  78. openedx_content/backcompat/publishing/migrations/0001_initial.py +166 -0
  79. openedx_content/backcompat/publishing/migrations/0002_alter_learningpackage_key_and_more.py +25 -0
  80. openedx_content/backcompat/publishing/migrations/0003_containers.py +54 -0
  81. openedx_content/backcompat/publishing/migrations/0004_publishableentity_can_stand_alone.py +21 -0
  82. openedx_content/backcompat/publishing/migrations/0005_alter_entitylistrow_options.py +17 -0
  83. openedx_content/backcompat/publishing/migrations/0006_draftchangelog.py +68 -0
  84. openedx_content/backcompat/publishing/migrations/0007_bootstrap_draftchangelog.py +94 -0
  85. openedx_content/backcompat/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +31 -0
  86. openedx_content/backcompat/publishing/migrations/0009_dependencies_and_hashing.py +62 -0
  87. openedx_content/backcompat/publishing/migrations/0010_backfill_dependencies.py +432 -0
  88. openedx_content/backcompat/publishing/migrations/0011_remove_all_field_state_for_move_to_applet.py +288 -0
  89. openedx_content/backcompat/publishing/migrations/__init__.py +0 -0
  90. openedx_content/backcompat/publishing/models.py +27 -0
  91. openedx_content/backcompat/sections/__init__.py +0 -0
  92. openedx_content/backcompat/sections/apps.py +15 -0
  93. openedx_content/backcompat/sections/migrations/0001_initial.py +36 -0
  94. openedx_content/backcompat/sections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  95. openedx_content/backcompat/sections/migrations/__init__.py +0 -0
  96. openedx_content/backcompat/subsections/__init__.py +0 -0
  97. openedx_content/backcompat/subsections/apps.py +15 -0
  98. openedx_content/backcompat/subsections/migrations/0001_initial.py +36 -0
  99. openedx_content/backcompat/subsections/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  100. openedx_content/backcompat/subsections/migrations/__init__.py +0 -0
  101. openedx_content/backcompat/units/__init__.py +0 -0
  102. openedx_content/backcompat/units/apps.py +15 -0
  103. openedx_content/backcompat/units/migrations/0001_initial.py +36 -0
  104. openedx_content/backcompat/units/migrations/0002_remove_all_field_state_for_move_to_applet.py +29 -0
  105. openedx_content/backcompat/units/migrations/__init__.py +0 -0
  106. openedx_content/management/__init__.py +0 -0
  107. openedx_content/management/commands/__init__.py +0 -0
  108. openedx_content/management/commands/add_assets_to_component.py +86 -0
  109. openedx_content/management/commands/lp_dump.py +69 -0
  110. openedx_content/management/commands/lp_load.py +57 -0
  111. openedx_content/migrations/0001_initial.py +654 -0
  112. openedx_content/migrations/0002_rename_tables_to_openedx_content.py +138 -0
  113. openedx_content/migrations/__init__.py +0 -0
  114. openedx_content/models.py +17 -0
  115. openedx_content/models_api.py +16 -0
  116. openedx_content/settings_api.py +38 -0
  117. openedx_core/__init__.py +9 -0
  118. openedx_core-0.34.0.dist-info/METADATA +152 -0
  119. openedx_core-0.34.0.dist-info/RECORD +181 -0
  120. openedx_core-0.34.0.dist-info/WHEEL +6 -0
  121. openedx_core-0.34.0.dist-info/licenses/LICENSE.txt +674 -0
  122. openedx_core-0.34.0.dist-info/top_level.txt +4 -0
  123. openedx_django_lib/__init__.py +6 -0
  124. openedx_django_lib/admin_utils.py +116 -0
  125. openedx_django_lib/collations.py +65 -0
  126. openedx_django_lib/fields.py +208 -0
  127. openedx_django_lib/managers.py +48 -0
  128. openedx_django_lib/validators.py +15 -0
  129. openedx_tagging/__init__.py +0 -0
  130. openedx_tagging/admin.py +44 -0
  131. openedx_tagging/api.py +525 -0
  132. openedx_tagging/apps.py +19 -0
  133. openedx_tagging/data.py +40 -0
  134. openedx_tagging/import_export/__init__.py +4 -0
  135. openedx_tagging/import_export/actions.py +452 -0
  136. openedx_tagging/import_export/api.py +232 -0
  137. openedx_tagging/import_export/exceptions.py +113 -0
  138. openedx_tagging/import_export/import_plan.py +218 -0
  139. openedx_tagging/import_export/parsers.py +326 -0
  140. openedx_tagging/import_export/tasks.py +43 -0
  141. openedx_tagging/import_export/template.csv +30 -0
  142. openedx_tagging/import_export/template.json +158 -0
  143. openedx_tagging/migrations/0001_initial.py +212 -0
  144. openedx_tagging/migrations/0001_squashed.py +152 -0
  145. openedx_tagging/migrations/0002_auto_20230718_2026.py +79 -0
  146. openedx_tagging/migrations/0003_auto_20230721_1238.py +76 -0
  147. openedx_tagging/migrations/0004_auto_20230723_2001.py +35 -0
  148. openedx_tagging/migrations/0005_language_taxonomy.py +31 -0
  149. openedx_tagging/migrations/0006_alter_objecttag_unique_together.py +16 -0
  150. openedx_tagging/migrations/0006_auto_20230802_1631.py +82 -0
  151. openedx_tagging/migrations/0007_tag_import_task_log_null_fix.py +19 -0
  152. openedx_tagging/migrations/0008_taxonomy_description_not_null.py +24 -0
  153. openedx_tagging/migrations/0009_alter_objecttag_object_id.py +20 -0
  154. openedx_tagging/migrations/0010_cleanups.py +32 -0
  155. openedx_tagging/migrations/0011_remove_required.py +22 -0
  156. openedx_tagging/migrations/0012_language_taxonomy.py +43 -0
  157. openedx_tagging/migrations/0013_tag_parent_blank.py +19 -0
  158. openedx_tagging/migrations/0014_minor_fixes.py +36 -0
  159. openedx_tagging/migrations/0015_taxonomy_export_id.py +38 -0
  160. openedx_tagging/migrations/0016_object_tag_export_id.py +62 -0
  161. openedx_tagging/migrations/0017_alter_tagimporttask_status.py +18 -0
  162. openedx_tagging/migrations/0018_objecttag_is_copied.py +18 -0
  163. openedx_tagging/migrations/__init__.py +0 -0
  164. openedx_tagging/models/__init__.py +6 -0
  165. openedx_tagging/models/base.py +996 -0
  166. openedx_tagging/models/import_export.py +150 -0
  167. openedx_tagging/models/system_defined.py +248 -0
  168. openedx_tagging/models/utils.py +82 -0
  169. openedx_tagging/py.typed +0 -0
  170. openedx_tagging/rest_api/__init__.py +0 -0
  171. openedx_tagging/rest_api/paginators.py +89 -0
  172. openedx_tagging/rest_api/urls.py +9 -0
  173. openedx_tagging/rest_api/utils.py +109 -0
  174. openedx_tagging/rest_api/v1/__init__.py +0 -0
  175. openedx_tagging/rest_api/v1/permissions.py +68 -0
  176. openedx_tagging/rest_api/v1/serializers.py +387 -0
  177. openedx_tagging/rest_api/v1/urls.py +27 -0
  178. openedx_tagging/rest_api/v1/views.py +935 -0
  179. openedx_tagging/rest_api/v1/views_import.py +51 -0
  180. openedx_tagging/rules.py +206 -0
  181. openedx_tagging/urls.py +10 -0
File without changes
@@ -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,3 @@
1
+ """
2
+ Django Admin pages for Backup Restore models (WIP)
3
+ """
@@ -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,3 @@
1
+ """
2
+ Core models for Backup Restore (WIP)
3
+ """
@@ -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