openedx-learning 0.27.1__py2.py3-none-any.whl → 0.29.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 (21) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/apps/authoring/backup_restore/api.py +19 -4
  3. openedx_learning/apps/authoring/backup_restore/management/commands/lp_dump.py +22 -4
  4. openedx_learning/apps/authoring/backup_restore/management/commands/lp_load.py +57 -0
  5. openedx_learning/apps/authoring/backup_restore/serializers.py +168 -0
  6. openedx_learning/apps/authoring/backup_restore/toml.py +203 -24
  7. openedx_learning/apps/authoring/backup_restore/zipper.py +1007 -16
  8. openedx_learning/apps/authoring/components/api.py +55 -0
  9. openedx_learning/apps/authoring/components/migrations/0004_remove_componentversioncontent_uuid.py +17 -0
  10. openedx_learning/apps/authoring/components/models.py +1 -3
  11. openedx_learning/apps/authoring/publishing/api.py +65 -12
  12. openedx_learning/apps/authoring/sections/api.py +17 -0
  13. openedx_learning/apps/authoring/subsections/api.py +17 -0
  14. openedx_learning/apps/authoring/units/api.py +17 -0
  15. {openedx_learning-0.27.1.dist-info → openedx_learning-0.29.0.dist-info}/METADATA +14 -5
  16. {openedx_learning-0.27.1.dist-info → openedx_learning-0.29.0.dist-info}/RECORD +21 -18
  17. openedx_tagging/core/tagging/models/base.py +7 -5
  18. openedx_tagging/core/tagging/models/utils.py +37 -9
  19. {openedx_learning-0.27.1.dist-info → openedx_learning-0.29.0.dist-info}/WHEEL +0 -0
  20. {openedx_learning-0.27.1.dist-info → openedx_learning-0.29.0.dist-info}/licenses/LICENSE.txt +0 -0
  21. {openedx_learning-0.27.1.dist-info → openedx_learning-0.29.0.dist-info}/top_level.txt +0 -0
@@ -34,6 +34,7 @@ from .models import Component, ComponentType, ComponentVersion, ComponentVersion
34
34
  # to be callable only by other apps in the authoring package.
35
35
  __all__ = [
36
36
  "get_or_create_component_type",
37
+ "get_or_create_component_type_by_entity_key",
37
38
  "create_component",
38
39
  "create_component_version",
39
40
  "create_next_component_version",
@@ -73,6 +74,27 @@ def get_or_create_component_type(namespace: str, name: str) -> ComponentType:
73
74
  return component_type
74
75
 
75
76
 
77
+ def get_or_create_component_type_by_entity_key(entity_key: str) -> tuple[ComponentType, str]:
78
+ """
79
+ Get or create a ComponentType based on a full entity key string.
80
+
81
+ The entity key is expected to be in the format
82
+ ``"{namespace}:{type_name}:{local_key}"``. This function will parse out the
83
+ ``namespace`` and ``type_name`` parts and use those to get or create the
84
+ ComponentType.
85
+
86
+ Raises ValueError if the entity_key is not in the expected format.
87
+ """
88
+ try:
89
+ namespace, type_name, local_key = entity_key.split(':', 2)
90
+ except ValueError as exc:
91
+ raise ValueError(
92
+ f"Invalid entity_key format: {entity_key!r}. "
93
+ "Expected format: '{namespace}:{type_name}:{local_key}'"
94
+ ) from exc
95
+ return get_or_create_component_type(namespace, type_name), local_key
96
+
97
+
76
98
  def create_component(
77
99
  learning_package_id: int,
78
100
  /,
@@ -137,10 +159,28 @@ def create_next_component_version(
137
159
  created: datetime,
138
160
  title: str | None = None,
139
161
  created_by: int | None = None,
162
+ *,
163
+ force_version_num: int | None = None,
164
+ ignore_previous_content: bool = False,
140
165
  ) -> ComponentVersion:
141
166
  """
142
167
  Create a new ComponentVersion based on the most recent version.
143
168
 
169
+ Args:
170
+ component_pk (int): The primary key of the Component to version.
171
+ content_to_replace (dict): Mapping of file keys to Content IDs,
172
+ None (for deletion), or bytes (for new file content).
173
+ created (datetime): The creation timestamp for the new version.
174
+ title (str, optional): Title for the new version. If None, uses the previous version's title.
175
+ created_by (int, optional): User ID of the creator.
176
+ force_version_num (int, optional): If provided, overrides the automatic version number increment and sets
177
+ this version's number explicitly. Use this if you need to restore or import a version with a specific
178
+ version number, such as during data migration or when synchronizing with external systems.
179
+ ignore_previous_content (bool): If True, do not copy over content from the previous version.
180
+
181
+ Returns:
182
+ ComponentVersion: The newly created ComponentVersion instance.
183
+
144
184
  A very common pattern for making a new ComponentVersion is going to be "make
145
185
  it just like the last version, except changing these one or two things".
146
186
  Before calling this, you should create any new contents via the contents
@@ -161,6 +201,14 @@ def create_next_component_version(
161
201
  convenient to remove paths (e.g. due to deprecation) without having to
162
202
  always check for its existence first.
163
203
 
204
+ Why use force_version_num?
205
+ Normally, the version number is incremented automatically from the latest version. If you need to set a specific
206
+ version number (for example, when restoring from backup, importing legacy data, or synchronizing with another
207
+ system), use force_version_num to override the default behavior.
208
+
209
+ Why not use create_component_version?
210
+ The main reason is that we want to reuse the logic to create a static file component from a dictionary.
211
+
164
212
  TODO: Have to add learning_downloadable info to this when it comes time to
165
213
  support static asset download.
166
214
  """
@@ -180,6 +228,9 @@ def create_next_component_version(
180
228
  if title is None:
181
229
  title = last_version.title
182
230
 
231
+ if force_version_num is not None:
232
+ next_version_num = force_version_num
233
+
183
234
  with atomic():
184
235
  publishable_entity_version = publishing_api.create_publishable_entity_version(
185
236
  component_pk,
@@ -219,6 +270,10 @@ def create_next_component_version(
219
270
  component_version=component_version,
220
271
  key=key,
221
272
  )
273
+
274
+ if ignore_previous_content:
275
+ return component_version
276
+
222
277
  # Now copy any old associations that existed, as long as they aren't
223
278
  # in conflict with the new stuff or marked for deletion.
224
279
  last_version_content_mapping = ComponentVersionContent.objects \
@@ -0,0 +1,17 @@
1
+ # Generated by Django 4.2.23 on 2025-10-02 14:00
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('oel_components', '0003_remove_componentversioncontent_learner_downloadable'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='componentversioncontent',
15
+ name='uuid',
16
+ ),
17
+ ]
@@ -21,7 +21,7 @@ from typing import ClassVar
21
21
 
22
22
  from django.db import models
23
23
 
24
- from ....lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field
24
+ from ....lib.fields import case_sensitive_char_field, key_field
25
25
  from ....lib.managers import WithRelationsManager
26
26
  from ..contents.models import Content
27
27
  from ..publishing.models import LearningPackage, PublishableEntityMixin, PublishableEntityVersionMixin
@@ -240,8 +240,6 @@ class ComponentVersionContent(models.Model):
240
240
  component_version = models.ForeignKey(ComponentVersion, on_delete=models.CASCADE)
241
241
  content = models.ForeignKey(Content, on_delete=models.RESTRICT)
242
242
 
243
- uuid = immutable_uuid_field()
244
-
245
243
  # "key" is a reserved word for MySQL, so we're temporarily using the column
246
244
  # name of "_key" to avoid breaking downstream tooling. A possible
247
245
  # alternative name for this would be "path", since it's most often used as
@@ -10,7 +10,7 @@ from contextlib import nullcontext
10
10
  from dataclasses import dataclass
11
11
  from datetime import datetime, timezone
12
12
  from enum import Enum
13
- from typing import ContextManager, TypeVar
13
+ from typing import ContextManager, Optional, TypeVar
14
14
 
15
15
  from django.core.exceptions import ObjectDoesNotExist, ValidationError
16
16
  from django.db.models import F, Q, QuerySet
@@ -58,9 +58,9 @@ __all__ = [
58
58
  "create_publishable_entity_version",
59
59
  "get_publishable_entity",
60
60
  "get_publishable_entity_by_key",
61
+ "get_publishable_entities",
61
62
  "get_last_publish",
62
63
  "get_all_drafts",
63
- "get_entities",
64
64
  "get_entities_with_unpublished_changes",
65
65
  "get_entities_with_unpublished_deletes",
66
66
  "publish_all_drafts",
@@ -89,6 +89,7 @@ __all__ = [
89
89
  "get_containers_with_entity",
90
90
  "get_container_children_count",
91
91
  "bulk_draft_changes_for",
92
+ "get_container_children_entities_keys",
92
93
  ]
93
94
 
94
95
 
@@ -262,11 +263,18 @@ def get_all_drafts(learning_package_id: int, /) -> QuerySet[Draft]:
262
263
  )
263
264
 
264
265
 
265
- def get_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
266
+ def get_publishable_entities(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
266
267
  """
267
268
  Get all entities in a learning package.
268
269
  """
269
- return PublishableEntity.objects.filter(learning_package_id=learning_package_id)
270
+ return (
271
+ PublishableEntity.objects
272
+ .filter(learning_package_id=learning_package_id)
273
+ .select_related(
274
+ "draft__version",
275
+ "published__version",
276
+ )
277
+ )
270
278
 
271
279
 
272
280
  def get_entities_with_unpublished_changes(
@@ -425,15 +433,22 @@ def publish_from_drafts(
425
433
  return publish_log
426
434
 
427
435
 
428
- def get_draft_version(publishable_entity_id: int, /) -> PublishableEntityVersion | None:
436
+ def get_draft_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
429
437
  """
430
438
  Return current draft PublishableEntityVersion for this PublishableEntity.
431
439
 
432
440
  This function will return None if there is no current draft.
433
441
  """
442
+ if isinstance(publishable_entity_or_id, PublishableEntity):
443
+ # Fetches the draft version for a given PublishableEntity.
444
+ # Gracefully handles cases where no draft is present.
445
+ draft: Optional[Draft] = getattr(publishable_entity_or_id, "draft", None)
446
+ if draft is None:
447
+ return None
448
+ return draft.version
434
449
  try:
435
450
  draft = Draft.objects.select_related("version").get(
436
- entity_id=publishable_entity_id
451
+ entity_id=publishable_entity_or_id
437
452
  )
438
453
  except Draft.DoesNotExist:
439
454
  # No draft was ever created.
@@ -445,15 +460,22 @@ def get_draft_version(publishable_entity_id: int, /) -> PublishableEntityVersion
445
460
  return draft.version
446
461
 
447
462
 
448
- def get_published_version(publishable_entity_id: int, /) -> PublishableEntityVersion | None:
463
+ def get_published_version(publishable_entity_or_id: PublishableEntity | int, /) -> PublishableEntityVersion | None:
449
464
  """
450
465
  Return current published PublishableEntityVersion for this PublishableEntity.
451
466
 
452
467
  This function will return None if there is no current published version.
453
468
  """
469
+ if isinstance(publishable_entity_or_id, PublishableEntity):
470
+ # Fetches the published version for a given PublishableEntity.
471
+ # Gracefully handles cases where no published version is present.
472
+ published: Optional[Published] = getattr(publishable_entity_or_id, "published", None)
473
+ if published is None:
474
+ return None
475
+ return published.version
454
476
  try:
455
477
  published = Published.objects.select_related("version").get(
456
- entity_id=publishable_entity_id
478
+ entity_id=publishable_entity_or_id
457
479
  )
458
480
  except Published.DoesNotExist:
459
481
  return None
@@ -1120,6 +1142,7 @@ def create_next_container_version(
1120
1142
  created_by: int | None,
1121
1143
  container_version_cls: type[ContainerVersionModel] = ContainerVersion, # type: ignore[assignment]
1122
1144
  entities_action: ChildrenEntitiesAction = ChildrenEntitiesAction.REPLACE,
1145
+ force_version_num: int | None = None,
1123
1146
  ) -> ContainerVersionModel:
1124
1147
  """
1125
1148
  [ 🛑 UNSTABLE ]
@@ -1140,26 +1163,40 @@ def create_next_container_version(
1140
1163
  created: The date and time the container version was created.
1141
1164
  created_by: The ID of the user who created the container version.
1142
1165
  container_version_cls: The subclass of ContainerVersion to use, if applicable.
1166
+ force_version_num (int, optional): If provided, overrides the automatic version number increment and sets
1167
+ this version's number explicitly. Use this if you need to restore or import a version with a specific
1168
+ version number, such as during data migration or when synchronizing with external systems.
1143
1169
 
1144
1170
  Returns:
1145
1171
  The newly created container version.
1172
+
1173
+ Why use force_version_num?
1174
+ Normally, the version number is incremented automatically from the latest version.
1175
+ If you need to set a specific version number (for example, when restoring from backup,
1176
+ importing legacy data, or synchronizing with another system),
1177
+ use force_version_num to override the default behavior.
1146
1178
  """
1147
1179
  assert issubclass(container_version_cls, ContainerVersion)
1148
1180
  with atomic():
1149
1181
  container = Container.objects.select_related("publishable_entity").get(pk=container_pk)
1150
1182
  entity = container.publishable_entity
1151
1183
  last_version = container.versioning.latest
1152
- assert last_version is not None
1153
- next_version_num = last_version.version_num + 1
1184
+ if last_version is None:
1185
+ next_version_num = 1
1186
+ else:
1187
+ next_version_num = last_version.version_num + 1
1154
1188
 
1155
- if entity_rows is None:
1189
+ if force_version_num is not None:
1190
+ next_version_num = force_version_num
1191
+
1192
+ if entity_rows is None and last_version is not None:
1156
1193
  # We're only changing metadata. Keep the same entity list.
1157
1194
  next_entity_list = last_version.entity_list
1158
1195
  else:
1159
1196
  next_entity_list = create_next_entity_list(
1160
1197
  entity.learning_package_id,
1161
1198
  last_version,
1162
- entity_rows,
1199
+ entity_rows if entity_rows is not None else [],
1163
1200
  entities_action
1164
1201
  )
1165
1202
 
@@ -1447,6 +1484,22 @@ def get_container_children_count(
1447
1484
  return container_version.entity_list.entitylistrow_set.filter(**filter_deleted).count()
1448
1485
 
1449
1486
 
1487
+ def get_container_children_entities_keys(container_version: ContainerVersion) -> list[str]:
1488
+ """
1489
+ Fetch the list of entity keys for all entities in the given container version.
1490
+
1491
+ Args:
1492
+ container_version: The ContainerVersion to fetch the entity keys for.
1493
+ Returns:
1494
+ A list of entity keys for all entities in the container version, ordered by entity key.
1495
+ """
1496
+ return list(
1497
+ container_version.entity_list.entitylistrow_set
1498
+ .values_list("entity__key", flat=True)
1499
+ .order_by("order_num")
1500
+ )
1501
+
1502
+
1450
1503
  def bulk_draft_changes_for(
1451
1504
  learning_package_id: int,
1452
1505
  changed_by: int | None = None,
@@ -131,6 +131,7 @@ def create_next_section_version(
131
131
  created: datetime,
132
132
  created_by: int | None = None,
133
133
  entities_action: publishing_api.ChildrenEntitiesAction = publishing_api.ChildrenEntitiesAction.REPLACE,
134
+ force_version_num: int | None = None,
134
135
  ) -> SectionVersion:
135
136
  """
136
137
  [ 🛑 UNSTABLE ] Create the next section version.
@@ -142,6 +143,21 @@ def create_next_section_version(
142
143
  Passing None will leave the existing subsections unchanged.
143
144
  created: The creation date.
144
145
  created_by: The user who created the section.
146
+ force_version_num (int, optional): If provided, overrides the automatic version number increment and sets
147
+ this version's number explicitly. Use this if you need to restore or import a version with a specific
148
+ version number, such as during data migration or when synchronizing with external systems.
149
+
150
+ Returns:
151
+ The newly created SectionVersion.
152
+
153
+ Why use force_version_num?
154
+ Normally, the version number is incremented automatically from the latest version.
155
+ If you need to set a specific version number (for example, when restoring from backup,
156
+ importing legacy data, or synchronizing with another system),
157
+ use force_version_num to override the default behavior.
158
+
159
+ Why not use create_component_version?
160
+ The main reason is that we want to reuse the logic for adding entities to this container.
145
161
  """
146
162
  entity_rows = _pub_entities_for_subsections(subsections)
147
163
  section_version = publishing_api.create_next_container_version(
@@ -152,6 +168,7 @@ def create_next_section_version(
152
168
  created_by=created_by,
153
169
  container_version_cls=SectionVersion,
154
170
  entities_action=entities_action,
171
+ force_version_num=force_version_num,
155
172
  )
156
173
  return section_version
157
174
 
@@ -130,6 +130,7 @@ def create_next_subsection_version(
130
130
  created: datetime,
131
131
  created_by: int | None = None,
132
132
  entities_action: publishing_api.ChildrenEntitiesAction = publishing_api.ChildrenEntitiesAction.REPLACE,
133
+ force_version_num: int | None = None,
133
134
  ) -> SubsectionVersion:
134
135
  """
135
136
  [ 🛑 UNSTABLE ] Create the next subsection version.
@@ -141,6 +142,21 @@ def create_next_subsection_version(
141
142
  will leave the existing units unchanged.
142
143
  created: The creation date.
143
144
  created_by: The user who created the subsection.
145
+ force_version_num (int, optional): If provided, overrides the automatic version number increment and sets
146
+ this version's number explicitly. Use this if you need to restore or import a version with a specific
147
+ version number, such as during data migration or when synchronizing with external systems.
148
+
149
+ Returns:
150
+ The newly created subsection version.
151
+
152
+ Why use force_version_num?
153
+ Normally, the version number is incremented automatically from the latest version.
154
+ If you need to set a specific version number (for example, when restoring from backup,
155
+ importing legacy data, or synchronizing with another system),
156
+ use force_version_num to override the default behavior.
157
+
158
+ Why not use create_component_version?
159
+ The main reason is that we want to reuse the logic for adding entities to this container.
144
160
  """
145
161
  entity_rows = _pub_entities_for_units(units)
146
162
  subsection_version = publishing_api.create_next_container_version(
@@ -151,6 +167,7 @@ def create_next_subsection_version(
151
167
  created_by=created_by,
152
168
  container_version_cls=SubsectionVersion,
153
169
  entities_action=entities_action,
170
+ force_version_num=force_version_num,
154
171
  )
155
172
  return subsection_version
156
173
 
@@ -79,6 +79,21 @@ def create_unit_version(
79
79
  entity_rows: child entities/versions
80
80
  created: The creation date.
81
81
  created_by: The user who created the unit.
82
+ force_version_num (int, optional): If provided, overrides the automatic version number increment and sets
83
+ this version's number explicitly. Use this if you need to restore or import a version with a specific
84
+ version number, such as during data migration or when synchronizing with external systems.
85
+
86
+ Returns:
87
+ UnitVersion: The newly created UnitVersion instance.
88
+
89
+ Why use force_version_num?
90
+ Normally, the version number is incremented automatically from the latest version.
91
+ If you need to set a specific version number (for example, when restoring from backup,
92
+ importing legacy data, or synchronizing with another system),
93
+ use force_version_num to override the default behavior.
94
+
95
+ Why not use create_component_version?
96
+ The main reason is that we want to reuse the logic for adding entities to this container.
82
97
  """
83
98
  return publishing_api.create_container_version(
84
99
  unit.pk,
@@ -131,6 +146,7 @@ def create_next_unit_version(
131
146
  created: datetime,
132
147
  created_by: int | None = None,
133
148
  entities_action: publishing_api.ChildrenEntitiesAction = publishing_api.ChildrenEntitiesAction.REPLACE,
149
+ force_version_num: int | None = None,
134
150
  ) -> UnitVersion:
135
151
  """
136
152
  [ 🛑 UNSTABLE ] Create the next unit version.
@@ -152,6 +168,7 @@ def create_next_unit_version(
152
168
  created_by=created_by,
153
169
  container_version_cls=UnitVersion,
154
170
  entities_action=entities_action,
171
+ force_version_num=force_version_num,
155
172
  )
156
173
  return unit_version
157
174
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openedx-learning
3
- Version: 0.27.1
3
+ Version: 0.29.0
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -20,12 +20,12 @@ 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
24
- Requires-Dist: celery
25
- Requires-Dist: Django
26
- Requires-Dist: attrs
27
23
  Requires-Dist: edx-drf-extensions
24
+ Requires-Dist: attrs
25
+ Requires-Dist: celery
26
+ Requires-Dist: tomlkit
28
27
  Requires-Dist: rules<4.0
28
+ Requires-Dist: Django
29
29
  Dynamic: author
30
30
  Dynamic: author-email
31
31
  Dynamic: classifier
@@ -148,6 +148,15 @@ Every time you develop something in this repo
148
148
 
149
149
  # Open a PR and ask for review.
150
150
 
151
+ Configuring Visual Studio Code
152
+ ------------------------------
153
+
154
+ If you are using VS Code as your editor, you can enable the Testing bar by copying from the example configuration provided in the ``.vscode`` directory::
155
+
156
+ cd .vscode/
157
+ cp launch.json.example launch.json
158
+ cp settings.json.example settings.json
159
+
151
160
  License
152
161
  -------
153
162
 
@@ -1,4 +1,4 @@
1
- openedx_learning/__init__.py,sha256=yNCuSKMLv6YJnAu09cqc4GpR5uVIDaJP2-5z9RAWJeM,69
1
+ openedx_learning/__init__.py,sha256=EbTAmn_afptQWd58IITE0FNOrcbnrnZOFcv-6aCIBwQ,69
2
2
  openedx_learning/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  openedx_learning/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  openedx_learning/api/authoring.py,sha256=EDWTY_JDKtjD9nFrrijzWuVccs3LZeDLEdzTUNanR4I,1111
@@ -7,14 +7,16 @@ openedx_learning/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
7
7
  openedx_learning/apps/authoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  openedx_learning/apps/authoring/backup_restore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
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
10
+ openedx_learning/apps/authoring/backup_restore/api.py,sha256=TlQmb9VADDj8UlNjOczlwOfY8JVFfGyZ3UN5nluL9Tw,1243
11
11
  openedx_learning/apps/authoring/backup_restore/apps.py,sha256=UnExBA7jhd3qI30_87JMvzVhS_k82t89qDVKSMpvg_A,340
12
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
13
+ openedx_learning/apps/authoring/backup_restore/serializers.py,sha256=LBWrlWmA0Aok-XSfwiGBqNTU-1ig4hnEX_AxFaAjaoY,6487
14
+ openedx_learning/apps/authoring/backup_restore/toml.py,sha256=4QFKDoFXsEF3xr1gvRXKQien-4_GezH6Gn1BdYQF8yU,8286
15
+ openedx_learning/apps/authoring/backup_restore/zipper.py,sha256=rsmF-slvQy6e6SBCJjKwejw-iqb2hby60mHO2atRSKM,46078
15
16
  openedx_learning/apps/authoring/backup_restore/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  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/management/commands/lp_dump.py,sha256=6-8D1rnDajTAIJu1HZLayY87hTfMnkaumYgSmLWpiGE,2302
19
+ openedx_learning/apps/authoring/backup_restore/management/commands/lp_load.py,sha256=z-glaYCulllcptqFbX5cb0UL_b9NslaaHK-0kOmPLa4,2205
18
20
  openedx_learning/apps/authoring/backup_restore/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
21
  openedx_learning/apps/authoring/collections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
22
  openedx_learning/apps/authoring/collections/admin.py,sha256=f0hySjDMfIphVDEGkCSMIUHoEiqHRA7EE2NiO7lvL4g,1156
@@ -29,15 +31,16 @@ openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_opt
29
31
  openedx_learning/apps/authoring/collections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
32
  openedx_learning/apps/authoring/components/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
33
  openedx_learning/apps/authoring/components/admin.py,sha256=zfEpuBEySMYpUZzygaE2MDoI8SH-2H3xIL20YCSCMLo,4582
32
- openedx_learning/apps/authoring/components/api.py,sha256=nJZcGXN5gOz8EYX3XHCiDILowNRcm__RxTfTNt5rZZw,22780
34
+ openedx_learning/apps/authoring/components/api.py,sha256=fnc7PaxaQFsTkJHfQMsxu62V4wHGXt4I5fjJHT9xDsU,25386
33
35
  openedx_learning/apps/authoring/components/apps.py,sha256=hi1SF2Z8Ex0hgE82wJK5Z_vYYfbcRhtaUW1zWZCdJYI,786
34
- openedx_learning/apps/authoring/components/models.py,sha256=ttZzVnMZTa14-qudrLb4CFuCanEQJT8cuC_iVPH8XTA,10887
36
+ openedx_learning/apps/authoring/components/models.py,sha256=j0AXjCDSBqqBlhP_06aGEuLFiJkhsolA0FdA41NbCmg,10830
35
37
  openedx_learning/apps/authoring/components/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
38
  openedx_learning/apps/authoring/components/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py,sha256=0dJ77NZZoNzYheOdFPXtJrjdL_Z-pCNg3l1rbEGnMCY,3175
38
40
  openedx_learning/apps/authoring/components/migrations/0001_initial.py,sha256=446LkJSFeK8J_-l-bxakZ_BVx_CiJIllGcBYqWcEenA,4664
39
41
  openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py,sha256=98724dtucRjJCRyLt5p45qXYb2d6-ouVGp7PB6zTG6E,539
40
42
  openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py,sha256=hDAkKdBvKULepML9pVMqkZg31nAyCeszQHJsFJ4qGws,382
43
+ openedx_learning/apps/authoring/components/migrations/0004_remove_componentversioncontent_uuid.py,sha256=hmCUt9VkiisGPckXjVY_qDNNkyj3r6KMK-MpasgOf6w,384
41
44
  openedx_learning/apps/authoring/components/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
45
  openedx_learning/apps/authoring/contents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
46
  openedx_learning/apps/authoring/contents/admin.py,sha256=9Njd_lje1emcd168KBWUTGf0mVJ6K-dMYMcqHNjRU4k,1761
@@ -48,7 +51,7 @@ openedx_learning/apps/authoring/contents/migrations/0001_initial.py,sha256=FtOTm
48
51
  openedx_learning/apps/authoring/contents/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
52
  openedx_learning/apps/authoring/publishing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
53
  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
54
+ openedx_learning/apps/authoring/publishing/api.py,sha256=zTG88gVIWfw-oCxC1SLl64OXbA_V_Cd3KDwAAoJWcZM,59679
52
55
  openedx_learning/apps/authoring/publishing/apps.py,sha256=PXYIx-TwN7a8dDudodX80Z7hNV9bWzrMZnpDET8lCGE,758
53
56
  openedx_learning/apps/authoring/publishing/contextmanagers.py,sha256=AH5zhr0Tz_gUG9--dfr_oZAu8DMy94n6mnOJuPbWkeU,6723
54
57
  openedx_learning/apps/authoring/publishing/migrations/0001_initial.py,sha256=wvekNV19YRSdxRmQaFnLSn_nCsQlHIucPDVMmgKf_OE,9272
@@ -69,21 +72,21 @@ openedx_learning/apps/authoring/publishing/models/publish_log.py,sha256=QD7Fb00y
69
72
  openedx_learning/apps/authoring/publishing/models/publishable_entity.py,sha256=ErzsvCcYbvjnMvsPqErOgSob9Vpaa7nmykNPTSQkZk8,25894
70
73
  openedx_learning/apps/authoring/sections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
74
  openedx_learning/apps/authoring/sections/admin.py,sha256=OQOTtXYM-Zj8BBb1wNBkOxgkF2Pv3JdUrZ45VOEmThM,1757
72
- openedx_learning/apps/authoring/sections/api.py,sha256=MFAHxDqUqbXygY_L37Lo4uXrju0m4Kf9ozk4yZnDs6Y,10438
75
+ openedx_learning/apps/authoring/sections/api.py,sha256=DXFHCjR8hauJK0Cmwxtk-PQLQpoVPpna-USOYNeRenI,11405
73
76
  openedx_learning/apps/authoring/sections/apps.py,sha256=vbLhC3WIKmG1MD0mvxX01IjoF6xPiJoutJar-h_bH7Y,746
74
77
  openedx_learning/apps/authoring/sections/models.py,sha256=2GK-dDMJwNRw_9gNFho8iKcDV-iYz_zBzqGMDmQ_jbc,1450
75
78
  openedx_learning/apps/authoring/sections/migrations/0001_initial.py,sha256=iW5AFhC26mfZNWEVNu8cTsr32Ca4htL4CUanHoXfaeY,1152
76
79
  openedx_learning/apps/authoring/sections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
80
  openedx_learning/apps/authoring/subsections/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
81
  openedx_learning/apps/authoring/subsections/admin.py,sha256=vPfOeTzh10aRhtZjXAzYsbwfw4Hc5yuySbpjAvtDH98,1795
79
- openedx_learning/apps/authoring/subsections/api.py,sha256=cJHPZ2JbAL3aXa1rhV6R5_oIP_VmWWDCi9L-2qjifwo,10338
82
+ openedx_learning/apps/authoring/subsections/api.py,sha256=aoGLjSbf9cUcAFbPvENThKhbJvZlAuzYP-P6Q0G5duw,11309
80
83
  openedx_learning/apps/authoring/subsections/apps.py,sha256=awpHVtg6bwIF1sEMeVcaGMwvrVzckfEHOFA9eFt5900,781
81
84
  openedx_learning/apps/authoring/subsections/models.py,sha256=1uhdpS9Eg6keSqkzQaE8-XSVLAQlmi0llIIU2V7Nl44,1492
82
85
  openedx_learning/apps/authoring/subsections/migrations/0001_initial.py,sha256=7kEHIC-EwG2KvlW4hg5tnl45--dW4Yv5gqV5SDqNYNo,1158
83
86
  openedx_learning/apps/authoring/subsections/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
87
  openedx_learning/apps/authoring/units/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
88
  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
89
+ openedx_learning/apps/authoring/units/api.py,sha256=ERJhwkpcIKNOZogZ2R3-T8mv3ud9oQNgloQRv-7ld-k,10849
87
90
  openedx_learning/apps/authoring/units/apps.py,sha256=AlKOUoC5zPrRrEedLvGzMf31ujWyhcaaoCNS4LI-u50,709
88
91
  openedx_learning/apps/authoring/units/models.py,sha256=eTOwFWC9coQLf0ovx08Mj7zi8mPAWCw9QOznybajRk0,1418
89
92
  openedx_learning/apps/authoring/units/migrations/0001_initial.py,sha256=qM_0JGffxECVgXzncHXfgSE-g8u3L3a14R0M1Bnj_Ys,1129
@@ -101,7 +104,7 @@ openedx_learning/lib/fields.py,sha256=eiGoXMPhRuq25EH2qf6BAODshAQE3DBVdIYAMIUAXW
101
104
  openedx_learning/lib/managers.py,sha256=-Q3gxalSqyPZ9Im4DTROW5tF8wVTZLlmfTe62_xmowY,1643
102
105
  openedx_learning/lib/test_utils.py,sha256=g3KLuepIZbaDBCsaj9711YuqyUx7LD4gXDcfNC-mWdc,527
103
106
  openedx_learning/lib/validators.py,sha256=iqEdEAvFV2tC7Ecssx69kjecpdU8nE87AlDJYrqrsnc,404
104
- openedx_learning-0.27.1.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
107
+ openedx_learning-0.29.0.dist-info/licenses/LICENSE.txt,sha256=QTW2QN7q3XszgUAXm9Dzgtu5LXYKbR1SGnqMa7ufEuY,35139
105
108
  openedx_tagging/__init__.py,sha256=V9N8M7f9LYlAbA_DdPUsHzTnWjYRXKGa5qHw9P1JnNI,30
106
109
  openedx_tagging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
110
  openedx_tagging/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -143,10 +146,10 @@ openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py,sha25
143
146
  openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py,sha256=zmr4b65T0vX6fYc8MpvSmQnYkAiNMpx3RKEd5tudsl8,517
144
147
  openedx_tagging/core/tagging/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
145
148
  openedx_tagging/core/tagging/models/__init__.py,sha256=yYdOnthuc7EUdfEULtZgqRwn5Y4bbYQmJCjVZqR5GTM,236
146
- openedx_tagging/core/tagging/models/base.py,sha256=ju4mvgRS_I2AgPsRf4sMFy6qle2i0aA0MbyBYZXf32g,39685
149
+ openedx_tagging/core/tagging/models/base.py,sha256=V2Qyp4L2QhTs6lZwB05_H54W6yWvyIEHhTcAGJAq-BY,39845
147
150
  openedx_tagging/core/tagging/models/import_export.py,sha256=Aj0pleh0nh2LNS6zmdB1P4bpdgUMmvmobTkqBerORAI,4570
148
151
  openedx_tagging/core/tagging/models/system_defined.py,sha256=_6LfvUZGEltvQMtm2OXy6TOLh3C8GnVTqtZDSAZW6K4,9062
149
- openedx_tagging/core/tagging/models/utils.py,sha256=-A3Dj24twmTf65UB7G4WLvb_9qEvduEPIwahZ-FJDlg,1926
152
+ openedx_tagging/core/tagging/models/utils.py,sha256=VhNL2cUTbCuxhsEsLXEDcgdV2tK2amUwVdWxmeZDr2w,2925
150
153
  openedx_tagging/core/tagging/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
154
  openedx_tagging/core/tagging/rest_api/paginators.py,sha256=BUIAg3taihHx7uAjpTZAGK1xSZzZY9G0aib4OKv5c0k,2651
152
155
  openedx_tagging/core/tagging/rest_api/urls.py,sha256=egXaRQv1EAgF04ThgVZBQuvLK1LimuyUKKBD2Hbqb10,148
@@ -157,7 +160,7 @@ openedx_tagging/core/tagging/rest_api/v1/serializers.py,sha256=0HQD_Jrf6-YpocYfz
157
160
  openedx_tagging/core/tagging/rest_api/v1/urls.py,sha256=dNUKCtUCx_YzrwlbEbpDfjGVQbb2QdJ1VuJCkladj6E,752
158
161
  openedx_tagging/core/tagging/rest_api/v1/views.py,sha256=Hf92cy-tE767DE9FgsZcPKiCYrf5ihfETz8qGKBnuiU,36278
159
162
  openedx_tagging/core/tagging/rest_api/v1/views_import.py,sha256=kbHUPe5A6WaaJ3J1lFIcYCt876ecLNQfd19m7YYub6c,1470
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,,
163
+ openedx_learning-0.29.0.dist-info/METADATA,sha256=125LwgRiSIAljrMgqzku24Zjf5Gejjsbia9FMjxXieA,9372
164
+ openedx_learning-0.29.0.dist-info/WHEEL,sha256=JNWh1Fm1UdwIQV075glCn4MVuCRs0sotJIq-J6rbxCU,109
165
+ openedx_learning-0.29.0.dist-info/top_level.txt,sha256=IYFbr5mgiEHd-LOtZmXj3q3a0bkGK1M9LY7GXgnfi4M,33
166
+ openedx_learning-0.29.0.dist-info/RECORD,,
@@ -465,7 +465,7 @@ class Taxonomy(models.Model):
465
465
  if include_counts:
466
466
  return qs.annotate(usage_count=models.Count("value"))
467
467
  else:
468
- return qs.distinct()
468
+ return qs.distinct() # type: ignore[return-value]
469
469
 
470
470
  def _get_filtered_tags_one_level(
471
471
  self,
@@ -486,12 +486,14 @@ class Taxonomy(models.Model):
486
486
  # Use parent_tag.value not parent_tag_value because they may differ in case
487
487
  qs = qs.annotate(parent_value=Value(parent_tag.value))
488
488
  else:
489
- qs = self.tag_set.filter(parent=None).annotate(depth=Value(0))
489
+ qs = self.tag_set.filter(parent=None).annotate(depth=Value(0)) # type: ignore[no-redef]
490
490
  qs = qs.annotate(parent_value=Value(None, output_field=models.CharField()))
491
- qs = qs.annotate(child_count=models.Count("children", distinct=True))
491
+ qs = qs.annotate(child_count=models.Count("children", distinct=True)) # type: ignore[no-redef]
492
492
  qs = qs.annotate(grandchild_count=models.Count("children__children", distinct=True))
493
493
  qs = qs.annotate(great_grandchild_count=models.Count("children__children__children"))
494
- qs = qs.annotate(descendant_count=F("child_count") + F("grandchild_count") + F("great_grandchild_count"))
494
+ qs = qs.annotate(
495
+ descendant_count=F("child_count") + F("grandchild_count") + F("great_grandchild_count")
496
+ ) # type: ignore[no-redef]
495
497
  # Filter by search term:
496
498
  if search_term:
497
499
  qs = qs.filter(value__icontains=search_term)
@@ -597,7 +599,7 @@ class Taxonomy(models.Model):
597
599
  count=models.Func(F('id'), function='Count')
598
600
  )
599
601
  qs = qs.annotate(usage_count=models.Subquery(obj_tags.values('count')))
600
- return qs
602
+ return qs # type: ignore[return-value]
601
603
 
602
604
  def add_tag(
603
605
  self,