openedx-learning 0.10.0__tar.gz → 0.11.1__tar.gz

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 (119) hide show
  1. {openedx_learning-0.10.0/openedx_learning.egg-info → openedx_learning-0.11.1}/PKG-INFO +4 -5
  2. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/__init__.py +1 -1
  3. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/api/authoring.py +6 -0
  4. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/api/authoring_models.py +1 -0
  5. openedx_learning-0.11.1/openedx_learning/apps/authoring/collections/api.py +80 -0
  6. openedx_learning-0.11.1/openedx_learning/apps/authoring/collections/apps.py +15 -0
  7. openedx_learning-0.11.1/openedx_learning/apps/authoring/collections/migrations/0001_initial.py +33 -0
  8. openedx_learning-0.11.1/openedx_learning/apps/authoring/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +53 -0
  9. openedx_learning-0.11.1/openedx_learning/apps/authoring/collections/models.py +98 -0
  10. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/api.py +2 -2
  11. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/models.py +2 -0
  12. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/api.py +48 -9
  13. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/model_mixins.py +12 -0
  14. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/collations.py +0 -45
  15. {openedx_learning-0.10.0 → openedx_learning-0.11.1/openedx_learning.egg-info}/PKG-INFO +4 -5
  16. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning.egg-info/SOURCES.txt +7 -0
  17. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/api.py +4 -2
  18. openedx_learning-0.11.1/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
  19. openedx_learning-0.11.1/openedx_tagging/py.typed +0 -0
  20. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/setup.py +1 -2
  21. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/CHANGELOG.rst +0 -0
  22. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/LICENSE.txt +0 -0
  23. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/MANIFEST.in +0 -0
  24. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/README.rst +0 -0
  25. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/api/__init__.py +0 -0
  26. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/__init__.py +0 -0
  27. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/__init__.py +0 -0
  28. {openedx_learning-0.10.0/openedx_learning/apps/authoring/components → openedx_learning-0.11.1/openedx_learning/apps/authoring/collections}/__init__.py +0 -0
  29. {openedx_learning-0.10.0/openedx_learning/apps/authoring/components → openedx_learning-0.11.1/openedx_learning/apps/authoring/collections}/migrations/__init__.py +0 -0
  30. {openedx_learning-0.10.0/openedx_learning/apps/authoring/contents → openedx_learning-0.11.1/openedx_learning/apps/authoring/components}/__init__.py +0 -0
  31. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/admin.py +0 -0
  32. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/apps.py +0 -0
  33. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/migrations/0001_initial.py +0 -0
  34. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
  35. {openedx_learning-0.10.0/openedx_learning/apps/authoring/contents → openedx_learning-0.11.1/openedx_learning/apps/authoring/components}/migrations/__init__.py +0 -0
  36. {openedx_learning-0.10.0/openedx_learning/apps/authoring/publishing → openedx_learning-0.11.1/openedx_learning/apps/authoring/contents}/__init__.py +0 -0
  37. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/contents/admin.py +0 -0
  38. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/contents/api.py +0 -0
  39. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/contents/apps.py +0 -0
  40. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/contents/migrations/0001_initial.py +0 -0
  41. {openedx_learning-0.10.0/openedx_learning/apps/authoring/publishing → openedx_learning-0.11.1/openedx_learning/apps/authoring/contents}/migrations/__init__.py +0 -0
  42. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/contents/models.py +0 -0
  43. {openedx_learning-0.10.0/openedx_learning/contrib → openedx_learning-0.11.1/openedx_learning/apps/authoring/publishing}/__init__.py +0 -0
  44. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/admin.py +0 -0
  45. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/apps.py +0 -0
  46. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/migrations/0001_initial.py +0 -0
  47. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
  48. {openedx_learning-0.10.0/openedx_learning/lib → openedx_learning-0.11.1/openedx_learning/apps/authoring/publishing/migrations}/__init__.py +0 -0
  49. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/apps/authoring/publishing/models.py +0 -0
  50. {openedx_learning-0.10.0/openedx_tagging/core → openedx_learning-0.11.1/openedx_learning/contrib}/__init__.py +0 -0
  51. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/contrib/media_server/__init__.py +0 -0
  52. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/contrib/media_server/apps.py +0 -0
  53. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/contrib/media_server/urls.py +0 -0
  54. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/contrib/media_server/views.py +0 -0
  55. {openedx_learning-0.10.0/openedx_tagging/core/tagging → openedx_learning-0.11.1/openedx_learning/lib}/__init__.py +0 -0
  56. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/admin_utils.py +0 -0
  57. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/cache.py +0 -0
  58. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/fields.py +0 -0
  59. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/managers.py +0 -0
  60. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/test_utils.py +0 -0
  61. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/lib/validators.py +0 -0
  62. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning/py.typed +0 -0
  63. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning.egg-info/dependency_links.txt +0 -0
  64. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning.egg-info/not-zip-safe +0 -0
  65. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning.egg-info/requires.txt +2 -2
  66. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_learning.egg-info/top_level.txt +0 -0
  67. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/__init__.py +0 -0
  68. {openedx_learning-0.10.0/openedx_tagging/core/tagging/migrations → openedx_learning-0.11.1/openedx_tagging/core}/__init__.py +0 -0
  69. {openedx_learning-0.10.0/openedx_tagging/core/tagging/rest_api → openedx_learning-0.11.1/openedx_tagging/core/tagging}/__init__.py +0 -0
  70. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/admin.py +0 -0
  71. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/apps.py +0 -0
  72. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/data.py +0 -0
  73. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
  74. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
  75. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/api.py +0 -0
  76. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
  77. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
  78. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
  79. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
  80. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
  81. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/import_export/template.json +0 -0
  82. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
  83. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
  84. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
  85. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
  86. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
  87. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
  88. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
  89. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
  90. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
  91. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
  92. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
  93. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
  94. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
  95. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
  96. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
  97. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
  98. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +0 -0
  99. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py +0 -0
  100. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py +0 -0
  101. {openedx_learning-0.10.0/openedx_tagging/core/tagging/rest_api/v1 → openedx_learning-0.11.1/openedx_tagging/core/tagging/migrations}/__init__.py +0 -0
  102. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/models/__init__.py +0 -0
  103. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/models/base.py +0 -0
  104. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/models/import_export.py +0 -0
  105. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
  106. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/models/utils.py +0 -0
  107. /openedx_learning-0.10.0/openedx_tagging/py.typed → /openedx_learning-0.11.1/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
  108. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
  109. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
  110. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
  111. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
  112. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -0
  113. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
  114. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/v1/views.py +0 -0
  115. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
  116. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/rules.py +0 -0
  117. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/openedx_tagging/core/tagging/urls.py +0 -0
  118. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/requirements/base.in +0 -0
  119. {openedx_learning-0.10.0 → openedx_learning-0.11.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openedx-learning
3
- Version: 0.10.0
3
+ Version: 0.11.1
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -14,17 +14,16 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
15
15
  Classifier: Natural Language :: English
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
- Requires-Python: >=3.8
19
+ Requires-Python: >=3.11
21
20
  License-File: LICENSE.txt
22
21
  Requires-Dist: rules<4.0
23
- Requires-Dist: Django<5.0
24
22
  Requires-Dist: edx-drf-extensions
25
- Requires-Dist: attrs
26
23
  Requires-Dist: celery
24
+ Requires-Dist: attrs
27
25
  Requires-Dist: djangorestframework<4.0
26
+ Requires-Dist: Django<5.0
28
27
 
29
28
  Open edX Learning Core (and Tagging)
30
29
  ====================================
@@ -1,4 +1,4 @@
1
1
  """
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
- __version__ = "0.10.0"
4
+ __version__ = "0.11.1"
@@ -9,6 +9,12 @@ APIs.
9
9
  """
10
10
  # These wildcard imports are okay because these api modules declare __all__.
11
11
  # pylint: disable=wildcard-import
12
+ from ..apps.authoring.collections.api import *
12
13
  from ..apps.authoring.components.api import *
13
14
  from ..apps.authoring.contents.api import *
14
15
  from ..apps.authoring.publishing.api import *
16
+
17
+ # This was renamed after the authoring API refactoring pushed this and other
18
+ # app APIs into the openedx_learning.api.authoring module. Here I'm aliasing to
19
+ # it's previous name, to make migration a little easier.
20
+ create_next_version = create_next_component_version
@@ -7,6 +7,7 @@ consistent.
7
7
  """
8
8
  # These wildcard imports are okay because these modules declare __all__.
9
9
  # pylint: disable=wildcard-import
10
+ from ..apps.authoring.collections.models import *
10
11
  from ..apps.authoring.components.models import *
11
12
  from ..apps.authoring.contents.models import *
12
13
  from ..apps.authoring.publishing.model_mixins import *
@@ -0,0 +1,80 @@
1
+ """
2
+ Collections API (warning: UNSTABLE, in progress API)
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from django.db.models import QuerySet
7
+
8
+ from .models import Collection
9
+
10
+ # The public API that will be re-exported by openedx_learning.apps.authoring.api
11
+ # is listed in the __all__ entries below. Internal helper functions that are
12
+ # private to this module should start with an underscore. If a function does not
13
+ # start with an underscore AND it is not in __all__, that function is considered
14
+ # to be callable only by other apps in the authoring package.
15
+ __all__ = [
16
+ "create_collection",
17
+ "get_collection",
18
+ "get_learning_package_collections",
19
+ "update_collection",
20
+ ]
21
+
22
+
23
+ def create_collection(
24
+ learning_package_id: int,
25
+ title: str,
26
+ created_by: int | None,
27
+ description: str = "",
28
+ ) -> Collection:
29
+ """
30
+ Create a new Collection
31
+ """
32
+ collection = Collection.objects.create(
33
+ learning_package_id=learning_package_id,
34
+ title=title,
35
+ created_by_id=created_by,
36
+ description=description,
37
+ )
38
+ return collection
39
+
40
+
41
+ def get_collection(collection_id: int) -> Collection:
42
+ """
43
+ Get a Collection by ID
44
+ """
45
+ return Collection.objects.get(id=collection_id)
46
+
47
+
48
+ def update_collection(
49
+ collection_id: int,
50
+ title: str | None = None,
51
+ description: str | None = None,
52
+ ) -> Collection:
53
+ """
54
+ Update a Collection
55
+ """
56
+ collection = Collection.objects.get(id=collection_id)
57
+
58
+ # If no changes were requested, there's nothing to update, so just return
59
+ # the Collection as-is
60
+ if all(field is None for field in [title, description]):
61
+ return collection
62
+
63
+ if title is not None:
64
+ collection.title = title
65
+ if description is not None:
66
+ collection.description = description
67
+
68
+ collection.save()
69
+ return collection
70
+
71
+
72
+ def get_learning_package_collections(learning_package_id: int) -> QuerySet[Collection]:
73
+ """
74
+ Get all collections for a given learning package
75
+
76
+ Only enabled collections are returned
77
+ """
78
+ return Collection.objects \
79
+ .filter(learning_package_id=learning_package_id, enabled=True) \
80
+ .select_related("learning_package")
@@ -0,0 +1,15 @@
1
+ """
2
+ Django metadata for the Collections Django application.
3
+ """
4
+ from django.apps import AppConfig
5
+
6
+
7
+ class CollectionsConfig(AppConfig):
8
+ """
9
+ Configuration for the Collections Django application.
10
+ """
11
+
12
+ name = "openedx_learning.apps.authoring.collections"
13
+ verbose_name = "Learning Core > Authoring > Collections"
14
+ default_auto_field = "django.db.models.BigAutoField"
15
+ label = "oel_collections"
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.14 on 2024-08-05 20:42
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+ import openedx_learning.lib.fields
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ ('oel_publishing', '0002_alter_learningpackage_key_and_more'),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='Collection',
20
+ fields=[
21
+ ('id', models.AutoField(primary_key=True, serialize=False)),
22
+ ('name', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, db_index=True, help_text='The name of the collection.', max_length=255)),
23
+ ('description', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, help_text='Provides extra information for the user about this collection.', max_length=10000)),
24
+ ('enabled', models.BooleanField(default=True, help_text='Whether the collection is enabled or not.')),
25
+ ('created', models.DateTimeField(auto_now_add=True)),
26
+ ('modified', models.DateTimeField(auto_now=True)),
27
+ ('learning_package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage')),
28
+ ],
29
+ options={
30
+ 'verbose_name_plural': 'Collections',
31
+ },
32
+ ),
33
+ ]
@@ -0,0 +1,53 @@
1
+ # Generated by Django 4.2.14 on 2024-08-14 14:20
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+ import openedx_learning.lib.fields
8
+ import openedx_learning.lib.validators
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+
13
+ dependencies = [
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ('oel_collections', '0001_initial'),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.RemoveField(
20
+ model_name='collection',
21
+ name='name',
22
+ ),
23
+ migrations.AddField(
24
+ model_name='collection',
25
+ name='created_by',
26
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
27
+ ),
28
+ migrations.AddField(
29
+ model_name='collection',
30
+ name='title',
31
+ field=openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, default='Collection', help_text='The title of the collection.', max_length=500),
32
+ preserve_default=False,
33
+ ),
34
+ migrations.AlterField(
35
+ model_name='collection',
36
+ name='created',
37
+ field=models.DateTimeField(auto_now_add=True, validators=[openedx_learning.lib.validators.validate_utc_datetime]),
38
+ ),
39
+ migrations.AlterField(
40
+ model_name='collection',
41
+ name='description',
42
+ field=openedx_learning.lib.fields.MultiCollationTextField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, default='', help_text='Provides extra information for the user about this collection.', max_length=10000),
43
+ ),
44
+ migrations.AlterField(
45
+ model_name='collection',
46
+ name='modified',
47
+ field=models.DateTimeField(auto_now=True, validators=[openedx_learning.lib.validators.validate_utc_datetime]),
48
+ ),
49
+ migrations.AddIndex(
50
+ model_name='collection',
51
+ index=models.Index(fields=['learning_package', 'title'], name='oel_collect_learnin_dfaf89_idx'),
52
+ ),
53
+ ]
@@ -0,0 +1,98 @@
1
+ """
2
+ Core models for Collections
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from django.conf import settings
7
+ from django.db import models
8
+ from django.utils.translation import gettext_lazy as _
9
+
10
+ from ....lib.fields import MultiCollationTextField, case_insensitive_char_field
11
+ from ....lib.validators import validate_utc_datetime
12
+ from ..publishing.models import LearningPackage
13
+
14
+ __all__ = [
15
+ "Collection",
16
+ ]
17
+
18
+
19
+ class Collection(models.Model):
20
+ """
21
+ Represents a collection of library components
22
+ """
23
+
24
+ id = models.AutoField(primary_key=True)
25
+
26
+ # Each collection belongs to a learning package
27
+ learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
28
+
29
+ title = case_insensitive_char_field(
30
+ null=False,
31
+ blank=False,
32
+ max_length=500,
33
+ help_text=_(
34
+ "The title of the collection."
35
+ ),
36
+ )
37
+
38
+ description = MultiCollationTextField(
39
+ blank=True,
40
+ null=False,
41
+ default="",
42
+ max_length=10_000,
43
+ help_text=_(
44
+ "Provides extra information for the user about this collection."
45
+ ),
46
+ db_collations={
47
+ "sqlite": "NOCASE",
48
+ "mysql": "utf8mb4_unicode_ci",
49
+ }
50
+ )
51
+
52
+ # We don't have api functions to handle the enabled field. This is a placeholder for future use and
53
+ # a way to "soft delete" collections.
54
+ enabled = models.BooleanField(
55
+ default=True,
56
+ help_text=_(
57
+ "Whether the collection is enabled or not."
58
+ ),
59
+ )
60
+
61
+ created_by = models.ForeignKey(
62
+ settings.AUTH_USER_MODEL,
63
+ on_delete=models.SET_NULL,
64
+ null=True,
65
+ blank=True,
66
+ )
67
+
68
+ created = models.DateTimeField(
69
+ auto_now_add=True,
70
+ validators=[
71
+ validate_utc_datetime,
72
+ ],
73
+ )
74
+
75
+ modified = models.DateTimeField(
76
+ auto_now=True,
77
+ validators=[
78
+ validate_utc_datetime,
79
+ ],
80
+ )
81
+
82
+ class Meta:
83
+ verbose_name_plural = "Collections"
84
+ indexes = [
85
+ models.Index(fields=["learning_package", "title"]),
86
+ ]
87
+
88
+ def __repr__(self) -> str:
89
+ """
90
+ Developer-facing representation of a Collection.
91
+ """
92
+ return str(self)
93
+
94
+ def __str__(self) -> str:
95
+ """
96
+ User-facing string representation of a Collection.
97
+ """
98
+ return f"<{self.__class__.__name__}> ({self.id}:{self.title})"
@@ -30,7 +30,7 @@ __all__ = [
30
30
  "get_or_create_component_type",
31
31
  "create_component",
32
32
  "create_component_version",
33
- "create_next_version",
33
+ "create_next_component_version",
34
34
  "create_component_and_version",
35
35
  "get_component",
36
36
  "get_component_by_key",
@@ -109,7 +109,7 @@ def create_component_version(
109
109
  return component_version
110
110
 
111
111
 
112
- def create_next_version(
112
+ def create_next_component_version(
113
113
  component_pk: int,
114
114
  /,
115
115
  title: str,
@@ -134,6 +134,8 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
134
134
  'publishable_entity__draft__version__componentversion',
135
135
  'publishable_entity__published__version',
136
136
  'publishable_entity__published__version__componentversion',
137
+ 'publishable_entity__published__publish_log_record',
138
+ 'publishable_entity__published__publish_log_record__publish_log',
137
139
  )
138
140
 
139
141
  # This foreign key is technically redundant because we're already locked to
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from datetime import datetime, timezone
10
10
 
11
11
  from django.core.exceptions import ObjectDoesNotExist
12
- from django.db.models import F, QuerySet
12
+ from django.db.models import F, Q, QuerySet
13
13
  from django.db.transaction import atomic
14
14
 
15
15
  from .model_mixins import PublishableContentModelRegistry, PublishableEntityMixin, PublishableEntityVersionMixin
@@ -216,10 +216,36 @@ def get_all_drafts(learning_package_id: int, /) -> QuerySet[Draft]:
216
216
  )
217
217
 
218
218
 
219
- def get_entities_with_unpublished_changes(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
220
- return PublishableEntity.objects \
221
- .filter(learning_package_id=learning_package_id) \
222
- .exclude(draft__version=F('published__version'))
219
+ def get_entities_with_unpublished_changes(
220
+ learning_package_id: int,
221
+ /,
222
+ include_deleted_drafts: bool = False
223
+ ) -> QuerySet[PublishableEntity]:
224
+ """
225
+ Fetch entities that have unpublished changes.
226
+
227
+ By default, this excludes soft-deleted drafts but can be included using include_deleted_drafts option.
228
+ """
229
+ entities_qs = (
230
+ PublishableEntity.objects
231
+ .filter(learning_package_id=learning_package_id)
232
+ .exclude(draft__version=F('published__version'))
233
+ )
234
+
235
+ if include_deleted_drafts:
236
+ # This means that we should also return PublishableEntities where the draft
237
+ # has been soft-deleted, but that deletion has not been published yet. Just
238
+ # excluding records where the Draft and Published versions don't match won't
239
+ # be enough here, because that will return soft-deletes that have already
240
+ # been published (since NULL != NULL in SQL).
241
+ #
242
+ # So we explicitly exclude already-published soft-deletes:
243
+ return entities_qs.exclude(
244
+ Q(draft__version__isnull=True) & Q(published__version__isnull=True)
245
+ )
246
+
247
+ # Simple case: exclude all entities that have been soft-deleted.
248
+ return entities_qs.exclude(draft__version__isnull=True)
223
249
 
224
250
 
225
251
  def get_entities_with_unpublished_deletes(learning_package_id: int, /) -> QuerySet[PublishableEntity]:
@@ -245,10 +271,23 @@ def publish_all_drafts(
245
271
  """
246
272
  Publish everything that is a Draft and is not already published.
247
273
  """
248
- draft_qset = Draft.objects \
249
- .select_related("entity__published") \
250
- .filter(entity__learning_package_id=learning_package_id) \
251
- .exclude(entity__published__version_id=F("version_id"))
274
+ draft_qset = (
275
+ Draft.objects.select_related("entity__published")
276
+ .filter(entity__learning_package_id=learning_package_id)
277
+
278
+ # Exclude entities where the Published version already matches the
279
+ # Draft version.
280
+ .exclude(entity__published__version_id=F("version_id"))
281
+
282
+ # Account for soft-deletes:
283
+ # NULL != NULL in SQL, so simply excluding entities where the Draft
284
+ # and Published versions match will not catch the case where a
285
+ # soft-delete has been published (i.e. both the Draft and Published
286
+ # versions are NULL). We need to explicitly check for that case
287
+ # instead, or else we will re-publish the same soft-deletes over
288
+ # and over again.
289
+ .exclude(Q(version__isnull=True) & Q(entity__published__version__isnull=True))
290
+ )
252
291
  return publish_from_drafts(
253
292
  learning_package_id, draft_qset, message, published_at, published_by
254
293
  )
@@ -243,6 +243,18 @@ class PublishableEntityMixin(models.Model):
243
243
 
244
244
  return draft_version_id != published_version_id
245
245
 
246
+ @property
247
+ def last_publish_log(self):
248
+ """
249
+ Return the most recent PublishLog for this component.
250
+
251
+ Return None if the component is not published.
252
+ """
253
+ pub_entity = self.content_obj.publishable_entity
254
+ if hasattr(pub_entity, 'published'):
255
+ return pub_entity.published.publish_log_record.publish_log
256
+ return None
257
+
246
258
  @property
247
259
  def versions(self):
248
260
  """
@@ -36,48 +36,6 @@ class MultiCollationMixin:
36
36
  super().__init__(*args, **kwargs)
37
37
  self.db_collations = db_collations or {}
38
38
 
39
- # This is part of a hack to get this to work for Django < 4.1. Please
40
- # see comments in the db_collation method for details.
41
- self._vendor = None
42
-
43
- @property
44
- def db_collation(self):
45
- """
46
- Return the db_collation, understanding that it varies by vendor.
47
-
48
- This method is a hack for Django 3.2 compatibility and should be removed
49
- after we move to 4.2.
50
-
51
- Description of why this is hacky:
52
-
53
- In Django 4.2, the schema builder pulls the collation settings from the
54
- field using the value returned from the ``db_parameters`` method, and
55
- this does what we want it to do. In Django 3.2, field.db_parameters is
56
- called, but any collation value sent back is ignored and the code grabs
57
- the value of db_collation directly from the field:
58
-
59
- https://github.com/django/django/blob/stable/3.2.x/django/db/backends/base/schema.py#L214-L224
60
-
61
- But this call to get the ``field.db_collation`` attribute happens almost
62
- immediately after the ``field.db_parameters`` method call. So our
63
- fragile hack is to set ``self._vendor`` in the ``db_parameters`` method,
64
- using the value we get from the connection that is passed in there. We
65
- can then use ``self._vendor`` to return the right value when Django
66
- calls ``field.db_collation`` (which is this property method).
67
-
68
- This method, the corresponding setter, and all references to
69
- ``self._vendor`` should be removed after we've cut over to Django 4.2.
70
- """
71
- return self.db_collations.get(self._vendor)
72
-
73
- @db_collation.setter
74
- def db_collation(self, value):
75
- """
76
- Don't allow db_collation to be set manually (just ignore).
77
-
78
- This can be removed when we move to Django 4.2.
79
- """
80
-
81
39
  def db_parameters(self, connection):
82
40
  """
83
41
  Return database parameters for this field. This adds collation info.
@@ -88,9 +46,6 @@ class MultiCollationMixin:
88
46
  """
89
47
  db_params = models.Field.db_parameters(self, connection)
90
48
 
91
- # Remove once we no longer need to support Django < 4.1
92
- self._vendor = connection.vendor
93
-
94
49
  # Now determine collation based on DB vendor (e.g. 'sqlite', 'mysql')
95
50
  if connection.vendor in self.db_collations:
96
51
  db_params["collation"] = self.db_collations[connection.vendor]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: openedx-learning
3
- Version: 0.10.0
3
+ Version: 0.11.1
4
4
  Summary: Open edX Learning Core and Tagging.
5
5
  Home-page: https://github.com/openedx/openedx-learning
6
6
  Author: David Ormsbee
@@ -14,17 +14,16 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
15
15
  Classifier: Natural Language :: English
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
- Requires-Python: >=3.8
19
+ Requires-Python: >=3.11
21
20
  License-File: LICENSE.txt
22
21
  Requires-Dist: rules<4.0
23
- Requires-Dist: Django<5.0
24
22
  Requires-Dist: edx-drf-extensions
25
- Requires-Dist: attrs
26
23
  Requires-Dist: celery
24
+ Requires-Dist: attrs
27
25
  Requires-Dist: djangorestframework<4.0
26
+ Requires-Dist: Django<5.0
28
27
 
29
28
  Open edX Learning Core (and Tagging)
30
29
  ====================================
@@ -17,6 +17,13 @@ openedx_learning/api/authoring.py
17
17
  openedx_learning/api/authoring_models.py
18
18
  openedx_learning/apps/__init__.py
19
19
  openedx_learning/apps/authoring/__init__.py
20
+ openedx_learning/apps/authoring/collections/__init__.py
21
+ openedx_learning/apps/authoring/collections/api.py
22
+ openedx_learning/apps/authoring/collections/apps.py
23
+ openedx_learning/apps/authoring/collections/models.py
24
+ openedx_learning/apps/authoring/collections/migrations/0001_initial.py
25
+ openedx_learning/apps/authoring/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py
26
+ openedx_learning/apps/authoring/collections/migrations/__init__.py
20
27
  openedx_learning/apps/authoring/components/__init__.py
21
28
  openedx_learning/apps/authoring/components/admin.py
22
29
  openedx_learning/apps/authoring/components/api.py
@@ -190,8 +190,10 @@ def get_object_tags(
190
190
  .exclude(taxonomy__enabled=False) # Exclude if the whole taxonomy is disabled
191
191
  )
192
192
  if not include_deleted:
193
- base_qs = base_qs.exclude(taxonomy_id=None) # Exclude if the whole taxonomy was deleted
194
- base_qs = base_qs.exclude(tag_id=None, taxonomy__allow_free_text=False) # Exclude if just the tag is deleted
193
+ # Exclude if the whole taxonomy was deleted
194
+ base_qs = base_qs.exclude(taxonomy_id=None) # type: ignore
195
+ # Exclude if just the tag is deleted
196
+ base_qs = base_qs.exclude(tag_id=None, taxonomy__allow_free_text=False) # type: ignore
195
197
  tags = (
196
198
  base_qs
197
199
  # Preload related objects, including data for the "get_lineage" method on ObjectTag/Tag:
File without changes
@@ -78,7 +78,7 @@ setup(
78
78
  ),
79
79
  include_package_data=True,
80
80
  install_requires=load_requirements('requirements/base.in'),
81
- python_requires=">=3.8",
81
+ python_requires=">=3.11",
82
82
  license="AGPL 3.0",
83
83
  zip_safe=False,
84
84
  keywords='Python edx',
@@ -90,7 +90,6 @@ setup(
90
90
  'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
91
91
  'Natural Language :: English',
92
92
  'Programming Language :: Python :: 3',
93
- 'Programming Language :: Python :: 3.8',
94
93
  'Programming Language :: Python :: 3.11',
95
94
  'Programming Language :: Python :: 3.12',
96
95
  ],
@@ -1,6 +1,6 @@
1
1
  rules<4.0
2
- Django<5.0
3
2
  edx-drf-extensions
4
- attrs
5
3
  celery
4
+ attrs
6
5
  djangorestframework<4.0
6
+ Django<5.0