openedx-learning 0.16.1__tar.gz → 0.17.0__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.
- {openedx_learning-0.16.1/openedx_learning.egg-info → openedx_learning-0.17.0}/PKG-INFO +4 -4
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/__init__.py +1 -1
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/admin.py +0 -1
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/api.py +28 -42
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py +3 -27
- openedx_learning-0.17.0/openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py +17 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/models.py +0 -37
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/models.py +8 -2
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/views.py +1 -6
- {openedx_learning-0.16.1 → openedx_learning-0.17.0/openedx_learning.egg-info}/PKG-INFO +4 -4
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/SOURCES.txt +1 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/CHANGELOG.rst +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/LICENSE.txt +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/MANIFEST.in +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/README.rst +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/api/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/api/authoring.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/api/authoring_models.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/admin.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/api.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/0001_initial.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/0002_remove_collection_name_collection_created_by_and_more.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/0003_collection_entities.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/0004_collection_key.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/0005_alter_collection_options_alter_collection_enabled.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/migrations/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/collections/models.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/management/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/management/commands/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/migrations/0001_initial.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/components/migrations/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/admin.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/api.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/migrations/0001_initial.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/migrations/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/admin.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/api.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/migrations/0001_initial.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/migrations/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/model_mixins.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/publishing/models.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/urls.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/admin_utils.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/cache.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/collations.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/fields.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/managers.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/test_utils.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/lib/validators.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/py.typed +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/dependency_links.txt +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/not-zip-safe +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/requires.txt +3 -3
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/top_level.txt +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/admin.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/api.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/apps.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/data.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/api.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/import_export/template.json +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0016_object_tag_export_id.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0017_alter_tagimporttask_status.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/0018_objecttag_is_copied.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/base.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/import_export.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/utils.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/views.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rules.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/urls.py +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/py.typed +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/requirements/base.in +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/setup.cfg +0 -0
- {openedx_learning-0.16.1 → openedx_learning-0.17.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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
|
|
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
License-File: LICENSE.txt
|
|
21
|
+
Requires-Dist: Django<5.0
|
|
22
|
+
Requires-Dist: djangorestframework<4.0
|
|
23
|
+
Requires-Dist: edx-drf-extensions
|
|
21
24
|
Requires-Dist: attrs
|
|
22
25
|
Requires-Dist: celery
|
|
23
26
|
Requires-Dist: rules<4.0
|
|
24
|
-
Requires-Dist: Django<5.0
|
|
25
|
-
Requires-Dist: edx-drf-extensions
|
|
26
|
-
Requires-Dist: djangorestframework<4.0
|
|
27
27
|
|
|
28
28
|
Open edX Learning Core (and Tagging)
|
|
29
29
|
====================================
|
|
@@ -12,6 +12,7 @@ are stored in this app.
|
|
|
12
12
|
"""
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
import mimetypes
|
|
15
16
|
from datetime import datetime, timezone
|
|
16
17
|
from enum import StrEnum, auto
|
|
17
18
|
from logging import getLogger
|
|
@@ -129,7 +130,7 @@ def create_component_version(
|
|
|
129
130
|
def create_next_component_version(
|
|
130
131
|
component_pk: int,
|
|
131
132
|
/,
|
|
132
|
-
content_to_replace: dict[str, int | None],
|
|
133
|
+
content_to_replace: dict[str, int | None | bytes],
|
|
133
134
|
created: datetime,
|
|
134
135
|
title: str | None = None,
|
|
135
136
|
created_by: int | None = None,
|
|
@@ -140,11 +141,14 @@ def create_next_component_version(
|
|
|
140
141
|
A very common pattern for making a new ComponentVersion is going to be "make
|
|
141
142
|
it just like the last version, except changing these one or two things".
|
|
142
143
|
Before calling this, you should create any new contents via the contents
|
|
143
|
-
API
|
|
144
|
+
API or send the content bytes as part of ``content_to_replace`` values.
|
|
144
145
|
|
|
145
146
|
The ``content_to_replace`` dict is a mapping of strings representing the
|
|
146
|
-
local path/key for a file, to ``Content.id`` values. Using
|
|
147
|
-
a value in this dict means to delete that key in the next version.
|
|
147
|
+
local path/key for a file, to ``Content.id`` or content bytes values. Using
|
|
148
|
+
`None` for a value in this dict means to delete that key in the next version.
|
|
149
|
+
|
|
150
|
+
Make sure to wrap the function call on a atomic statement:
|
|
151
|
+
``with transaction.atomic():``
|
|
148
152
|
|
|
149
153
|
It is okay to mark entries for deletion that don't exist. For instance, if a
|
|
150
154
|
version has ``a.txt`` and ``b.txt``, sending a ``content_to_replace`` value
|
|
@@ -186,16 +190,31 @@ def create_next_component_version(
|
|
|
186
190
|
component_id=component_pk,
|
|
187
191
|
)
|
|
188
192
|
# First copy the new stuff over...
|
|
189
|
-
for key,
|
|
193
|
+
for key, content_pk_or_bytes in content_to_replace.items():
|
|
190
194
|
# If the content_pk is None, it means we want to remove the
|
|
191
195
|
# content represented by our key from the next version. Otherwise,
|
|
192
196
|
# we add our key->content_pk mapping to the next version.
|
|
193
|
-
if
|
|
197
|
+
if content_pk_or_bytes is not None:
|
|
198
|
+
if isinstance(content_pk_or_bytes, bytes):
|
|
199
|
+
file_path, file_content = key, content_pk_or_bytes
|
|
200
|
+
media_type_str, _encoding = mimetypes.guess_type(file_path)
|
|
201
|
+
# We use "application/octet-stream" as a generic fallback media type, per
|
|
202
|
+
# RFC 2046: https://datatracker.ietf.org/doc/html/rfc2046
|
|
203
|
+
media_type_str = media_type_str or "application/octet-stream"
|
|
204
|
+
media_type = contents_api.get_or_create_media_type(media_type_str)
|
|
205
|
+
content = contents_api.get_or_create_file_content(
|
|
206
|
+
component.learning_package.id,
|
|
207
|
+
media_type.id,
|
|
208
|
+
data=file_content,
|
|
209
|
+
created=created,
|
|
210
|
+
)
|
|
211
|
+
content_pk = content.pk
|
|
212
|
+
else:
|
|
213
|
+
content_pk = content_pk_or_bytes
|
|
194
214
|
ComponentVersionContent.objects.create(
|
|
195
215
|
content_id=content_pk,
|
|
196
216
|
component_version=component_version,
|
|
197
217
|
key=key,
|
|
198
|
-
learner_downloadable=False,
|
|
199
218
|
)
|
|
200
219
|
# Now copy any old associations that existed, as long as they aren't
|
|
201
220
|
# in conflict with the new stuff or marked for deletion.
|
|
@@ -207,7 +226,6 @@ def create_next_component_version(
|
|
|
207
226
|
content_id=cvrc.content_id,
|
|
208
227
|
component_version=component_version,
|
|
209
228
|
key=cvrc.key,
|
|
210
|
-
learner_downloadable=cvrc.learner_downloadable,
|
|
211
229
|
)
|
|
212
230
|
|
|
213
231
|
return component_version
|
|
@@ -402,7 +420,6 @@ def create_component_version_content(
|
|
|
402
420
|
content_id: int,
|
|
403
421
|
/,
|
|
404
422
|
key: str,
|
|
405
|
-
learner_downloadable: bool = False,
|
|
406
423
|
) -> ComponentVersionContent:
|
|
407
424
|
"""
|
|
408
425
|
Add a Content to the given ComponentVersion
|
|
@@ -425,7 +442,6 @@ def create_component_version_content(
|
|
|
425
442
|
component_version_id=component_version_id,
|
|
426
443
|
content_id=content_id,
|
|
427
444
|
key=key,
|
|
428
|
-
learner_downloadable=learner_downloadable,
|
|
429
445
|
)
|
|
430
446
|
return cvrc
|
|
431
447
|
|
|
@@ -433,7 +449,6 @@ def create_component_version_content(
|
|
|
433
449
|
class AssetError(StrEnum):
|
|
434
450
|
"""Error codes related to fetching ComponentVersion assets."""
|
|
435
451
|
ASSET_PATH_NOT_FOUND_FOR_COMPONENT_VERSION = auto()
|
|
436
|
-
ASSET_NOT_LEARNER_DOWNLOADABLE = auto()
|
|
437
452
|
ASSET_HAS_NO_DOWNLOAD_FILE = auto()
|
|
438
453
|
|
|
439
454
|
|
|
@@ -464,7 +479,6 @@ def get_redirect_response_for_component_asset(
|
|
|
464
479
|
component_version_uuid: UUID,
|
|
465
480
|
asset_path: Path,
|
|
466
481
|
public: bool = False,
|
|
467
|
-
learner_downloadable_only: bool = True,
|
|
468
482
|
) -> HttpResponse:
|
|
469
483
|
"""
|
|
470
484
|
``HttpResponse`` for a reverse-proxy to serve a ``ComponentVersion`` asset.
|
|
@@ -478,11 +492,6 @@ def get_redirect_response_for_component_asset(
|
|
|
478
492
|
If ``True``, this will return an ``HttpResponse`` that can be cached in
|
|
479
493
|
a CDN and shared across many clients.
|
|
480
494
|
|
|
481
|
-
:param learner_downloadable_only: Only return assets that are meant to be
|
|
482
|
-
downloadable by Learners, i.e. in the LMS experience. If this is
|
|
483
|
-
``True``, then requests for assets that are not meant for student
|
|
484
|
-
download will return a ``404`` error response.
|
|
485
|
-
|
|
486
495
|
**Response Codes**
|
|
487
496
|
|
|
488
497
|
If the asset exists for this ``ComponentVersion``, this function will return
|
|
@@ -492,15 +501,10 @@ def get_redirect_response_for_component_asset(
|
|
|
492
501
|
the ``ComponentVersion`` itself does not exist, the response code will be
|
|
493
502
|
``404``.
|
|
494
503
|
|
|
495
|
-
|
|
496
|
-
*this function does not do auth checking of any sort*–it will never return
|
|
504
|
+
This function does not do auth checking of any sort. It will never return
|
|
497
505
|
a ``401`` or ``403`` response code. That is by design. Figuring out who is
|
|
498
506
|
making the request and whether they have permission to do so is the
|
|
499
|
-
responsiblity of whatever is calling this function.
|
|
500
|
-
``learner_downloadable_only`` flag is intended to be a filter for the entire
|
|
501
|
-
view. When it's True, not even staff can download component-internal assets.
|
|
502
|
-
This is intended to protect us from accidentally allowing sensitive grading
|
|
503
|
-
code to get leaked out.
|
|
507
|
+
responsiblity of whatever is calling this function.
|
|
504
508
|
|
|
505
509
|
**Metadata Headers**
|
|
506
510
|
|
|
@@ -576,24 +580,6 @@ def get_redirect_response_for_component_asset(
|
|
|
576
580
|
)
|
|
577
581
|
return HttpResponseNotFound(headers=info_headers)
|
|
578
582
|
|
|
579
|
-
# Check: If we're asking only for Learner Downloadable assets, and the asset
|
|
580
|
-
# in question is not supposed to be downloadable by learners, then we give a
|
|
581
|
-
# 404 error. Even staff members are not expected to be able to download
|
|
582
|
-
# these assets via the LMS endpoint that serves students. Studio would be
|
|
583
|
-
# expected to have an entirely different view to serve these assets in that
|
|
584
|
-
# context (along with different timeouts, auth, and cache settings). So in
|
|
585
|
-
# that sense, the asset doesn't exist for that particular endpoint.
|
|
586
|
-
if learner_downloadable_only and (not cv_content.learner_downloadable):
|
|
587
|
-
logger.error(
|
|
588
|
-
f"ComponentVersion {component_version_uuid} has asset {asset_path}, "
|
|
589
|
-
"but it is not meant to be downloadable by learners "
|
|
590
|
-
"(ComponentVersionContent.learner_downloadable=False)."
|
|
591
|
-
)
|
|
592
|
-
info_headers.update(
|
|
593
|
-
_error_header(AssetError.ASSET_NOT_LEARNER_DOWNLOADABLE)
|
|
594
|
-
)
|
|
595
|
-
return HttpResponseNotFound(headers=info_headers)
|
|
596
|
-
|
|
597
583
|
# At this point, we know that there is valid Content that we want to send.
|
|
598
584
|
# This adds Content-level headers, like the hash/etag and content type.
|
|
599
585
|
info_headers.update(contents_api.get_content_info_headers(content))
|
|
@@ -4,14 +4,11 @@ Management command to add files to a Component.
|
|
|
4
4
|
This is mostly meant to be a debugging tool to let us to easily load some test
|
|
5
5
|
asset data into the system.
|
|
6
6
|
"""
|
|
7
|
-
import mimetypes
|
|
8
7
|
import pathlib
|
|
9
8
|
from datetime import datetime, timezone
|
|
10
9
|
|
|
11
10
|
from django.core.management.base import BaseCommand
|
|
12
11
|
|
|
13
|
-
from ....components.api import create_component_version_content
|
|
14
|
-
from ....contents.api import get_or_create_file_content, get_or_create_media_type
|
|
15
12
|
from ....publishing.api import get_learning_package_by_key
|
|
16
13
|
from ...api import create_next_component_version, get_component_by_key
|
|
17
14
|
|
|
@@ -69,39 +66,18 @@ class Command(BaseCommand):
|
|
|
69
66
|
)
|
|
70
67
|
|
|
71
68
|
created = datetime.now(tz=timezone.utc)
|
|
72
|
-
|
|
73
|
-
local_keys_to_content = {}
|
|
69
|
+
local_keys_to_content_bytes = {}
|
|
74
70
|
|
|
75
71
|
for file_mapping in file_mappings:
|
|
76
72
|
local_key, file_path = file_mapping.split(":", 1)
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
if not file_path:
|
|
80
|
-
keys_to_remove.add(local_key)
|
|
81
|
-
continue
|
|
82
|
-
|
|
83
|
-
media_type_str, _encoding = mimetypes.guess_type(file_path)
|
|
84
|
-
media_type = get_or_create_media_type(media_type_str)
|
|
85
|
-
content = get_or_create_file_content(
|
|
86
|
-
learning_package.id,
|
|
87
|
-
media_type.id,
|
|
88
|
-
data=pathlib.Path(file_path).read_bytes(),
|
|
89
|
-
created=created,
|
|
90
|
-
)
|
|
91
|
-
local_keys_to_content[local_key] = content.id
|
|
74
|
+
local_keys_to_content_bytes[local_key] = pathlib.Path(file_path).read_bytes() if file_path else None
|
|
92
75
|
|
|
93
76
|
next_version = create_next_component_version(
|
|
94
77
|
component.pk,
|
|
95
|
-
content_to_replace=
|
|
78
|
+
content_to_replace=local_keys_to_content_bytes,
|
|
96
79
|
created=created,
|
|
97
80
|
)
|
|
98
|
-
for local_key, content_id in sorted(local_keys_to_content.items()):
|
|
99
|
-
create_component_version_content(
|
|
100
|
-
next_version.pk,
|
|
101
|
-
content_id,
|
|
102
|
-
key=local_key,
|
|
103
|
-
learner_downloadable=True,
|
|
104
|
-
)
|
|
105
81
|
|
|
106
82
|
self.stdout.write(
|
|
107
83
|
f"Created v{next_version.version_num} of "
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-11-06 17:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('oel_components', '0002_alter_componentversioncontent_key'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.RemoveField(
|
|
14
|
+
model_name='componentversioncontent',
|
|
15
|
+
name='learner_downloadable',
|
|
16
|
+
),
|
|
17
|
+
]
|
|
@@ -254,43 +254,6 @@ class ComponentVersionContent(models.Model):
|
|
|
254
254
|
# identifiers that don't map as cleanly to file paths at some point.
|
|
255
255
|
key = key_field(db_column="_key")
|
|
256
256
|
|
|
257
|
-
# Long explanation for the ``learner_downloadable`` field:
|
|
258
|
-
#
|
|
259
|
-
# Is this Content downloadable during the learning experience? This is
|
|
260
|
-
# NOT about public vs. private permissions on course assets, as that will be
|
|
261
|
-
# a policy that can be changed independently of new versions of the content.
|
|
262
|
-
# For instance, a course team could decide to flip their course assets from
|
|
263
|
-
# private to public for CDN caching reasons, and that should not require
|
|
264
|
-
# new ComponentVersions to be created.
|
|
265
|
-
#
|
|
266
|
-
# What the ``learner_downloadable`` field refers to is whether this asset is
|
|
267
|
-
# supposed to *ever* be directly downloadable by browsers during the
|
|
268
|
-
# learning experience. This will be True for things like images, PDFs, and
|
|
269
|
-
# video transcript files. This field will be False for things like:
|
|
270
|
-
#
|
|
271
|
-
# * Problem Block OLX will contain the answers to the problem. The XBlock
|
|
272
|
-
# runtime and ProblemBlock will use this information to generate HTML and
|
|
273
|
-
# grade responses, but the the user's browser is never permitted to
|
|
274
|
-
# actually download the raw OLX itself.
|
|
275
|
-
# * Many courses include a python_lib.zip file holding custom Python code
|
|
276
|
-
# to be used by codejail to assess student answers. This code will also
|
|
277
|
-
# potentially reveal answers, and is never intended to be downloadable by
|
|
278
|
-
# the student's browser.
|
|
279
|
-
# * Some course teams will upload other file formats that their OLX is
|
|
280
|
-
# derived from (e.g. specially formatted LaTeX files). These files will
|
|
281
|
-
# likewise contain answers and should never be downloadable by the
|
|
282
|
-
# student.
|
|
283
|
-
# * Other custom metadata may be attached as files in the import, such as
|
|
284
|
-
# custom identifiers, author information, etc.
|
|
285
|
-
#
|
|
286
|
-
# Even if ``learner_downloadble`` is True, the LMS may decide that this
|
|
287
|
-
# particular student isn't allowed to see this particular piece of content
|
|
288
|
-
# yet–e.g. because they are not enrolled, or because the exam this Component
|
|
289
|
-
# is a part of hasn't started yet. That's a matter of LMS permissions and
|
|
290
|
-
# policy that is not intrinsic to the content itself, and exists at a layer
|
|
291
|
-
# above this.
|
|
292
|
-
learner_downloadable = models.BooleanField(default=False)
|
|
293
|
-
|
|
294
257
|
class Meta:
|
|
295
258
|
constraints = [
|
|
296
259
|
# Uniqueness is only by ComponentVersion and key. If for some reason
|
|
@@ -6,6 +6,7 @@ more intelligent data models to be useful.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from functools import cache, cached_property
|
|
9
|
+
from logging import getLogger
|
|
9
10
|
|
|
10
11
|
from django.conf import settings
|
|
11
12
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
|
@@ -19,6 +20,8 @@ from ....lib.fields import MultiCollationTextField, case_insensitive_char_field,
|
|
|
19
20
|
from ....lib.managers import WithRelationsManager
|
|
20
21
|
from ..publishing.models import LearningPackage
|
|
21
22
|
|
|
23
|
+
logger = getLogger()
|
|
24
|
+
|
|
22
25
|
__all__ = [
|
|
23
26
|
"MediaType",
|
|
24
27
|
"Content",
|
|
@@ -316,8 +319,11 @@ class Content(models.Model):
|
|
|
316
319
|
|
|
317
320
|
This will return ``None`` if there is no backing file (has_file=False).
|
|
318
321
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
try:
|
|
323
|
+
if self.has_file:
|
|
324
|
+
return get_storage().path(self.path)
|
|
325
|
+
except NotImplementedError:
|
|
326
|
+
logger.warning("Storage backend does not support path()")
|
|
321
327
|
return None
|
|
322
328
|
|
|
323
329
|
def read_file(self) -> File:
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/views.py
RENAMED
|
@@ -5,7 +5,7 @@ Views for the media server application
|
|
|
5
5
|
"""
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from django.core.exceptions import ObjectDoesNotExist
|
|
8
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
9
9
|
from django.http import FileResponse, Http404
|
|
10
10
|
|
|
11
11
|
from openedx_learning.apps.authoring.components.api import look_up_component_version_content
|
|
@@ -34,11 +34,6 @@ def component_asset(
|
|
|
34
34
|
except ObjectDoesNotExist:
|
|
35
35
|
raise Http404("File not found") # pylint: disable=raise-missing-from
|
|
36
36
|
|
|
37
|
-
if not cvc.learner_downloadable and not (
|
|
38
|
-
request.user and request.user.is_superuser
|
|
39
|
-
):
|
|
40
|
-
raise PermissionDenied("This file is not publicly downloadable.")
|
|
41
|
-
|
|
42
37
|
response = FileResponse(cvc.raw_content.file, filename=Path(asset_path).name)
|
|
43
38
|
response["Content-Type"] = cvc.raw_content.mime_type
|
|
44
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: openedx-learning
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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
|
|
@@ -18,12 +18,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
20
|
License-File: LICENSE.txt
|
|
21
|
+
Requires-Dist: Django<5.0
|
|
22
|
+
Requires-Dist: djangorestframework<4.0
|
|
23
|
+
Requires-Dist: edx-drf-extensions
|
|
21
24
|
Requires-Dist: attrs
|
|
22
25
|
Requires-Dist: celery
|
|
23
26
|
Requires-Dist: rules<4.0
|
|
24
|
-
Requires-Dist: Django<5.0
|
|
25
|
-
Requires-Dist: edx-drf-extensions
|
|
26
|
-
Requires-Dist: djangorestframework<4.0
|
|
27
27
|
|
|
28
28
|
Open edX Learning Core (and Tagging)
|
|
29
29
|
====================================
|
|
@@ -38,6 +38,7 @@ openedx_learning/apps/authoring/components/management/commands/__init__.py
|
|
|
38
38
|
openedx_learning/apps/authoring/components/management/commands/add_assets_to_component.py
|
|
39
39
|
openedx_learning/apps/authoring/components/migrations/0001_initial.py
|
|
40
40
|
openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py
|
|
41
|
+
openedx_learning/apps/authoring/components/migrations/0003_remove_componentversioncontent_learner_downloadable.py
|
|
41
42
|
openedx_learning/apps/authoring/components/migrations/__init__.py
|
|
42
43
|
openedx_learning/apps/authoring/contents/__init__.py
|
|
43
44
|
openedx_learning/apps/authoring/contents/admin.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/api/authoring_models.py
RENAMED
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/api.py
RENAMED
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/apps/authoring/contents/apps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/apps.py
RENAMED
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning/contrib/media_server/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_learning.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/__init__.py
RENAMED
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/models/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/urls.py
RENAMED
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx_learning-0.16.1 → openedx_learning-0.17.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|