openedx-learning 0.3.7__tar.gz → 0.4.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.3.7/openedx_learning.egg-info → openedx-learning-0.4.0}/PKG-INFO +1 -1
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/__init__.py +1 -1
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/admin.py +1 -1
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/api.py +1 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/migrations/0001_initial.py +2 -2
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/admin.py +4 -4
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/api.py +37 -2
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/migrations/0001_initial.py +18 -6
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/models.py +88 -14
- openedx-learning-0.4.0/openedx_learning/lib/cache.py +34 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/lib/fields.py +10 -0
- openedx-learning-0.4.0/openedx_learning/lib/test_utils.py +22 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0/openedx_learning.egg-info}/PKG-INFO +1 -1
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/SOURCES.txt +2 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/actions.py +6 -4
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/api.py +1 -1
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/import_plan.py +10 -8
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/CHANGELOG.rst +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/LICENSE.txt +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/MANIFEST.in +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/README.rst +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/views.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/migrations/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/models.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/migrations/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/admin.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/api.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/migrations/0001_initial.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/migrations/0002_alter_fk_on_delete.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/migrations/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/model_mixins.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/models.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/lib/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/lib/admin_utils.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/lib/collations.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/lib/validators.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/v1/components.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/v1/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/dependency_links.txt +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/not-zip-safe +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/requires.txt +4 -4
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/top_level.txt +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/admin.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/api.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/apps.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/data.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/template.json +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/base.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/import_export.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/utils.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/utils.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/views.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rules.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/urls.py +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/requirements/base.in +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/setup.cfg +0 -0
- {openedx-learning-0.3.7 → openedx-learning-0.4.0}/setup.py +0 -0
|
@@ -158,7 +158,7 @@ def content_preview(cvc_obj: ComponentVersionRawContent) -> SafeText:
|
|
|
158
158
|
"""
|
|
159
159
|
raw_content_obj = cvc_obj.raw_content
|
|
160
160
|
|
|
161
|
-
if raw_content_obj.
|
|
161
|
+
if raw_content_obj.media_type.type == "image":
|
|
162
162
|
return format_html(
|
|
163
163
|
'<img src="{}" style="max-width: 100%;" />',
|
|
164
164
|
# TODO: configure with settings value:
|
|
@@ -117,6 +117,7 @@ def get_component_version_content(
|
|
|
117
117
|
"""
|
|
118
118
|
return ComponentVersionRawContent.objects.select_related(
|
|
119
119
|
"raw_content",
|
|
120
|
+
"raw_content__media_type",
|
|
120
121
|
"component_version",
|
|
121
122
|
"component_version__component",
|
|
122
123
|
"component_version__component__learning_package",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 3.2.
|
|
1
|
+
# Generated by Django 3.2.23 on 2023-12-04 00:41
|
|
2
2
|
|
|
3
3
|
import uuid
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ class Migration(migrations.Migration):
|
|
|
13
13
|
initial = True
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
|
-
('oel_publishing', '
|
|
16
|
+
('oel_publishing', '0002_alter_fk_on_delete'),
|
|
17
17
|
('oel_contents', '0001_initial'),
|
|
18
18
|
]
|
|
19
19
|
|
|
@@ -18,14 +18,14 @@ class RawContentAdmin(ReadOnlyModelAdmin):
|
|
|
18
18
|
"hash_digest",
|
|
19
19
|
"file_link",
|
|
20
20
|
"learning_package",
|
|
21
|
-
"
|
|
21
|
+
"media_type",
|
|
22
22
|
"size",
|
|
23
23
|
"created",
|
|
24
24
|
]
|
|
25
25
|
fields = [
|
|
26
26
|
"learning_package",
|
|
27
27
|
"hash_digest",
|
|
28
|
-
"
|
|
28
|
+
"media_type",
|
|
29
29
|
"size",
|
|
30
30
|
"created",
|
|
31
31
|
"file_link",
|
|
@@ -34,13 +34,13 @@ class RawContentAdmin(ReadOnlyModelAdmin):
|
|
|
34
34
|
readonly_fields = [
|
|
35
35
|
"learning_package",
|
|
36
36
|
"hash_digest",
|
|
37
|
-
"
|
|
37
|
+
"media_type",
|
|
38
38
|
"size",
|
|
39
39
|
"created",
|
|
40
40
|
"file_link",
|
|
41
41
|
"text_preview",
|
|
42
42
|
]
|
|
43
|
-
list_filter = ("
|
|
43
|
+
list_filter = ("media_type", "learning_package")
|
|
44
44
|
search_fields = ("hash_digest",)
|
|
45
45
|
|
|
46
46
|
def file_link(self, raw_content):
|
|
@@ -12,9 +12,10 @@ from datetime import datetime
|
|
|
12
12
|
from django.core.files.base import ContentFile
|
|
13
13
|
from django.db.transaction import atomic
|
|
14
14
|
|
|
15
|
+
from openedx_learning.lib.cache import lru_cache
|
|
15
16
|
from openedx_learning.lib.fields import create_hash_digest
|
|
16
17
|
|
|
17
|
-
from .models import RawContent, TextContent
|
|
18
|
+
from .models import MediaType, RawContent, TextContent
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def create_raw_content(
|
|
@@ -28,9 +29,10 @@ def create_raw_content(
|
|
|
28
29
|
Create a new RawContent instance and persist it to storage.
|
|
29
30
|
"""
|
|
30
31
|
hash_digest = hash_digest or create_hash_digest(data_bytes)
|
|
32
|
+
|
|
31
33
|
raw_content = RawContent.objects.create(
|
|
32
34
|
learning_package_id=learning_package_id,
|
|
33
|
-
|
|
35
|
+
media_type_id=get_media_type_id(mime_type),
|
|
34
36
|
hash_digest=hash_digest,
|
|
35
37
|
size=len(data_bytes),
|
|
36
38
|
created=created,
|
|
@@ -54,6 +56,39 @@ def create_text_from_raw_content(raw_content: RawContent, encoding="utf-8-sig")
|
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
@lru_cache(maxsize=128)
|
|
60
|
+
def get_media_type_id(mime_type: str) -> int:
|
|
61
|
+
"""
|
|
62
|
+
Return the MediaType.id for the desired mime_type string.
|
|
63
|
+
|
|
64
|
+
If it is not found in the database, a new entry will be created for it. This
|
|
65
|
+
lazy-writing means that MediaType entry IDs will *not* be the same across
|
|
66
|
+
different server instances, and apps should not assume that will be the
|
|
67
|
+
case. Even if we were to preload a bunch of common ones, we can't anticipate
|
|
68
|
+
the different XBlocks that will be installed in different server instances,
|
|
69
|
+
each of which will use their own MediaType.
|
|
70
|
+
|
|
71
|
+
This will typically only be called when create_raw_content is calling it to
|
|
72
|
+
lookup the media_type_id it should use for a new RawContent. If you already
|
|
73
|
+
have a RawContent instance, it makes much more sense to access its
|
|
74
|
+
media_type relation.
|
|
75
|
+
"""
|
|
76
|
+
if "+" in mime_type:
|
|
77
|
+
base, suffix = mime_type.split("+")
|
|
78
|
+
else:
|
|
79
|
+
base = mime_type
|
|
80
|
+
suffix = ""
|
|
81
|
+
|
|
82
|
+
main_type, sub_type = base.split("/")
|
|
83
|
+
mt, _created = MediaType.objects.get_or_create(
|
|
84
|
+
type=main_type,
|
|
85
|
+
sub_type=sub_type,
|
|
86
|
+
suffix=suffix,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return mt.id
|
|
90
|
+
|
|
91
|
+
|
|
57
92
|
def get_or_create_raw_content(
|
|
58
93
|
learning_package_id: int,
|
|
59
94
|
data_bytes: bytes,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 3.2.
|
|
1
|
+
# Generated by Django 3.2.23 on 2023-12-04 00:41
|
|
2
2
|
|
|
3
3
|
import django.core.validators
|
|
4
4
|
import django.db.models.deletion
|
|
@@ -13,20 +13,29 @@ class Migration(migrations.Migration):
|
|
|
13
13
|
initial = True
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
|
-
('oel_publishing', '
|
|
16
|
+
('oel_publishing', '0002_alter_fk_on_delete'),
|
|
17
17
|
]
|
|
18
18
|
|
|
19
19
|
operations = [
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='MediaType',
|
|
22
|
+
fields=[
|
|
23
|
+
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
24
|
+
('type', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
25
|
+
('sub_type', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
26
|
+
('suffix', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
27
|
+
],
|
|
28
|
+
),
|
|
20
29
|
migrations.CreateModel(
|
|
21
30
|
name='RawContent',
|
|
22
31
|
fields=[
|
|
23
32
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
24
33
|
('hash_digest', models.CharField(editable=False, max_length=40)),
|
|
25
|
-
('mime_type', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=255)),
|
|
26
34
|
('size', models.PositiveBigIntegerField(validators=[django.core.validators.MaxValueValidator(50000000)])),
|
|
27
35
|
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
|
|
28
36
|
('file', models.FileField(null=True, upload_to='')),
|
|
29
37
|
('learning_package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage')),
|
|
38
|
+
('media_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='oel_contents.mediatype')),
|
|
30
39
|
],
|
|
31
40
|
options={
|
|
32
41
|
'verbose_name': 'Raw Content',
|
|
@@ -41,10 +50,13 @@ class Migration(migrations.Migration):
|
|
|
41
50
|
('length', models.PositiveIntegerField()),
|
|
42
51
|
],
|
|
43
52
|
),
|
|
44
|
-
|
|
53
|
+
migrations.AddConstraint(
|
|
54
|
+
model_name='mediatype',
|
|
55
|
+
constraint=models.UniqueConstraint(fields=('type', 'sub_type', 'suffix'), name='oel_contents_uniq_t_st_sfx'),
|
|
56
|
+
),
|
|
45
57
|
migrations.AddIndex(
|
|
46
58
|
model_name='rawcontent',
|
|
47
|
-
index=models.Index(fields=['learning_package', '
|
|
59
|
+
index=models.Index(fields=['learning_package', 'media_type'], name='oel_content_idx_lp_media_type'),
|
|
48
60
|
),
|
|
49
61
|
migrations.AddIndex(
|
|
50
62
|
model_name='rawcontent',
|
|
@@ -56,6 +68,6 @@ class Migration(migrations.Migration):
|
|
|
56
68
|
),
|
|
57
69
|
migrations.AddConstraint(
|
|
58
70
|
model_name='rawcontent',
|
|
59
|
-
constraint=models.UniqueConstraint(fields=('learning_package', '
|
|
71
|
+
constraint=models.UniqueConstraint(fields=('learning_package', 'media_type', 'hash_digest'), name='oel_content_uniq_lc_media_type_hash_digest'),
|
|
60
72
|
),
|
|
61
73
|
]
|
|
@@ -3,6 +3,8 @@ These models are the most basic pieces of content we support. Think of them as
|
|
|
3
3
|
the simplest building blocks to store data with. They need to be composed into
|
|
4
4
|
more intelligent data models to be useful.
|
|
5
5
|
"""
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
|
|
6
8
|
from django.conf import settings
|
|
7
9
|
from django.core.files.storage import default_storage
|
|
8
10
|
from django.core.validators import MaxValueValidator
|
|
@@ -18,6 +20,81 @@ from openedx_learning.lib.fields import (
|
|
|
18
20
|
from ..publishing.models import LearningPackage
|
|
19
21
|
|
|
20
22
|
|
|
23
|
+
class MediaType(models.Model):
|
|
24
|
+
"""
|
|
25
|
+
Stores Media types for use by RawContent models.
|
|
26
|
+
|
|
27
|
+
This is the same as MIME types (the IANA renamed MIME Types to Media Types).
|
|
28
|
+
We don't pre-populate this table, so APIs that add RawContent must ensure
|
|
29
|
+
that the desired Media Type exists.
|
|
30
|
+
|
|
31
|
+
Media types are written as {type}/{sub_type}+{suffix}, where suffixes are
|
|
32
|
+
seldom used.
|
|
33
|
+
|
|
34
|
+
* application/json
|
|
35
|
+
* text/css
|
|
36
|
+
* image/svg+xml
|
|
37
|
+
* application/vnd.openedx.xblock.v1.problem+xml
|
|
38
|
+
|
|
39
|
+
We have this as a separate model (instead of a field on RawContent) because:
|
|
40
|
+
|
|
41
|
+
1. We can save a lot on storage and indexing for RawContent if we're just
|
|
42
|
+
storing foreign key references there, rather than the entire content
|
|
43
|
+
string to be indexed. This is especially relevant for our (long) custom
|
|
44
|
+
types like "application/vnd.openedx.xblock.v1.problem+xml".
|
|
45
|
+
2. These values can occasionally change. For instance, "text/javascript" vs.
|
|
46
|
+
"application/javascript". Also, we will be using a fair number of "vnd."
|
|
47
|
+
style of custom content types, and we may want the flexibility of
|
|
48
|
+
changing that without having to worry about migrating millions of rows of
|
|
49
|
+
RawContent.
|
|
50
|
+
"""
|
|
51
|
+
# We're going to have many foreign key references from RawContent into this
|
|
52
|
+
# model, and we don't need to store those as 8-byte BigAutoField, as is the
|
|
53
|
+
# default for this app. It's likely that a SmallAutoField would work, but I
|
|
54
|
+
# can just barely imagine using more than 32K Media types if we have a bunch
|
|
55
|
+
# of custom "vnd." entries, or start tracking suffixes and parameters. Which
|
|
56
|
+
# is how we end up at the 4-byte AutoField.
|
|
57
|
+
id = models.AutoField(primary_key=True)
|
|
58
|
+
|
|
59
|
+
# Media types are denoted as {type}/{sub_type}+{suffix}. We currently do not
|
|
60
|
+
# support parameters.
|
|
61
|
+
|
|
62
|
+
# Media type, e.g. "application", "text", "image". Per RFC 4288, this can be
|
|
63
|
+
# at most 127 chars long and is case insensitive. In practice, it's almost
|
|
64
|
+
# always written in lowercase.
|
|
65
|
+
type = case_insensitive_char_field(max_length=127, blank=False, null=False)
|
|
66
|
+
|
|
67
|
+
# Media sub-type, e.g. "json", "css", "png". Per RFC 4288, this can be at
|
|
68
|
+
# most 127 chars long and is case insensitive. In practice, it's almost
|
|
69
|
+
# always written in lowercase.
|
|
70
|
+
sub_type = case_insensitive_char_field(max_length=127, blank=False, null=False)
|
|
71
|
+
|
|
72
|
+
# Suffix, usually just "xml" (e.g. "image/svg+xml"). Usually blank. I
|
|
73
|
+
# couldn't find an RFC description of the length limit, and 127 is probably
|
|
74
|
+
# excessive. But this table should be small enough where it doesn't really
|
|
75
|
+
# matter.
|
|
76
|
+
suffix = case_insensitive_char_field(max_length=127, blank=True, null=False)
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
constraints = [
|
|
80
|
+
# Make sure all (type + sub_type + suffix) combinations are unique.
|
|
81
|
+
models.UniqueConstraint(
|
|
82
|
+
fields=[
|
|
83
|
+
"type",
|
|
84
|
+
"sub_type",
|
|
85
|
+
"suffix",
|
|
86
|
+
],
|
|
87
|
+
name="oel_contents_uniq_t_st_sfx",
|
|
88
|
+
),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def __str__(self):
|
|
92
|
+
base = f"{self.type}/{self.sub_type}"
|
|
93
|
+
if self.suffix:
|
|
94
|
+
return f"{base}+{self.suffix}"
|
|
95
|
+
return base
|
|
96
|
+
|
|
97
|
+
|
|
21
98
|
class RawContent(models.Model): # type: ignore[django-manager-missing]
|
|
22
99
|
"""
|
|
23
100
|
This is the most basic piece of raw content data, with no version metadata.
|
|
@@ -77,15 +154,8 @@ class RawContent(models.Model): # type: ignore[django-manager-missing]
|
|
|
77
154
|
# openedx.lib.fields module.
|
|
78
155
|
hash_digest = hash_field()
|
|
79
156
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
# in between). Also, while MIME types are almost always written in lowercase
|
|
83
|
-
# as a matter of convention, by spec they are NOT case sensitive.
|
|
84
|
-
#
|
|
85
|
-
# DO NOT STORE parameters here, e.g. "charset=". We can make a new field if
|
|
86
|
-
# that becomes necessary. If we do decide to store parameters and values
|
|
87
|
-
# later, note that those *may be* case sensitive.
|
|
88
|
-
mime_type = case_insensitive_char_field(max_length=255, blank=False)
|
|
157
|
+
# What is the Media type (a.k.a. MIME type) of this data?
|
|
158
|
+
media_type = models.ForeignKey(MediaType, on_delete=models.PROTECT)
|
|
89
159
|
|
|
90
160
|
# This is the size of the raw data file in bytes. This can be different than
|
|
91
161
|
# the character length, since UTF-8 encoding can use anywhere between 1-4
|
|
@@ -107,6 +177,10 @@ class RawContent(models.Model): # type: ignore[django-manager-missing]
|
|
|
107
177
|
storage=settings.OPENEDX_LEARNING.get("STORAGE", default_storage), # type: ignore
|
|
108
178
|
)
|
|
109
179
|
|
|
180
|
+
@cached_property
|
|
181
|
+
def mime_type(self):
|
|
182
|
+
return str(self.media_type)
|
|
183
|
+
|
|
110
184
|
class Meta:
|
|
111
185
|
constraints = [
|
|
112
186
|
# Make sure we don't store duplicates of this raw data within the
|
|
@@ -114,21 +188,21 @@ class RawContent(models.Model): # type: ignore[django-manager-missing]
|
|
|
114
188
|
models.UniqueConstraint(
|
|
115
189
|
fields=[
|
|
116
190
|
"learning_package",
|
|
117
|
-
"
|
|
191
|
+
"media_type",
|
|
118
192
|
"hash_digest",
|
|
119
193
|
],
|
|
120
|
-
name="
|
|
194
|
+
name="oel_content_uniq_lc_media_type_hash_digest",
|
|
121
195
|
),
|
|
122
196
|
]
|
|
123
197
|
indexes = [
|
|
124
|
-
# LearningPackage
|
|
198
|
+
# LearningPackage Media type Index:
|
|
125
199
|
# * Break down Content counts by type/subtype with in a
|
|
126
200
|
# LearningPackage.
|
|
127
201
|
# * Find all the Content in a LearningPackage that matches a
|
|
128
202
|
# certain MIME type (e.g. "image/png", "application/pdf".
|
|
129
203
|
models.Index(
|
|
130
|
-
fields=["learning_package", "
|
|
131
|
-
name="
|
|
204
|
+
fields=["learning_package", "media_type"],
|
|
205
|
+
name="oel_content_idx_lp_media_type",
|
|
132
206
|
),
|
|
133
207
|
# LearningPackage (reverse) Size Index:
|
|
134
208
|
# * Find largest Content in a LearningPackage.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test-friendly helpers for caching.
|
|
3
|
+
|
|
4
|
+
LRU caching can be especially helpful for Learning Core data models because so
|
|
5
|
+
much of it is immutable, e.g. Media Type lookups. But while these data models
|
|
6
|
+
are immutable within a given site, we also want to be able to track and clear
|
|
7
|
+
these caches across test runs. Later on, we may also want to inspect them to
|
|
8
|
+
make sure they are not growing overly large.
|
|
9
|
+
"""
|
|
10
|
+
import functools
|
|
11
|
+
|
|
12
|
+
# List of functions that have our
|
|
13
|
+
_lru_cached_fns = []
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lru_cache(*args, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Thin wrapper over functools.lru_cache that lets us clear all caches later.
|
|
19
|
+
"""
|
|
20
|
+
def decorator(fn):
|
|
21
|
+
wrapped_fn = functools.lru_cache(*args, **kwargs)(fn)
|
|
22
|
+
_lru_cached_fns.append(wrapped_fn)
|
|
23
|
+
return wrapped_fn
|
|
24
|
+
return decorator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clear_lru_caches():
|
|
28
|
+
"""
|
|
29
|
+
Clear all LRU caches that use our lru_cache decorator.
|
|
30
|
+
|
|
31
|
+
Useful for tests.
|
|
32
|
+
"""
|
|
33
|
+
for fn in _lru_cached_fns:
|
|
34
|
+
fn.cache_clear()
|
|
@@ -121,6 +121,16 @@ def hash_field() -> models.CharField:
|
|
|
121
121
|
|
|
122
122
|
Use the create_hash_digest function to generate data suitable for this
|
|
123
123
|
field.
|
|
124
|
+
|
|
125
|
+
There are a couple of ways that we could have stored this more efficiently,
|
|
126
|
+
but we don't at this time:
|
|
127
|
+
|
|
128
|
+
1. A BinaryField would be the most space efficient, but Django doesn't
|
|
129
|
+
support indexing a BinaryField in a MySQL database.
|
|
130
|
+
2. We could make the field case-sensitive and run it through a URL-safe
|
|
131
|
+
base64 encoding. But the amount of space this saves vs. the complexity
|
|
132
|
+
didn't seem worthwhile, particularly the possibility of case-sensitivity
|
|
133
|
+
related bugs.
|
|
124
134
|
"""
|
|
125
135
|
return models.CharField(
|
|
126
136
|
max_length=40,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test utilities for Learning Core.
|
|
3
|
+
|
|
4
|
+
The only thing here now is a TestCase class that knows how to clean up the
|
|
5
|
+
caching used by the cache module in this package.
|
|
6
|
+
"""
|
|
7
|
+
import django.test
|
|
8
|
+
|
|
9
|
+
from .cache import clear_lru_caches
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCase(django.test.TestCase):
|
|
13
|
+
"""
|
|
14
|
+
Subclass of Django's TestCase that knows how to reset caching we might use.
|
|
15
|
+
"""
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
clear_lru_caches()
|
|
18
|
+
super().setUp()
|
|
19
|
+
|
|
20
|
+
def tearDown(self) -> None:
|
|
21
|
+
clear_lru_caches()
|
|
22
|
+
super().tearDown()
|
|
@@ -42,8 +42,10 @@ openedx_learning/core/publishing/migrations/0002_alter_fk_on_delete.py
|
|
|
42
42
|
openedx_learning/core/publishing/migrations/__init__.py
|
|
43
43
|
openedx_learning/lib/__init__.py
|
|
44
44
|
openedx_learning/lib/admin_utils.py
|
|
45
|
+
openedx_learning/lib/cache.py
|
|
45
46
|
openedx_learning/lib/collations.py
|
|
46
47
|
openedx_learning/lib/fields.py
|
|
48
|
+
openedx_learning/lib/test_utils.py
|
|
47
49
|
openedx_learning/lib/validators.py
|
|
48
50
|
openedx_learning/rest_api/__init__.py
|
|
49
51
|
openedx_learning/rest_api/apps.py
|
|
@@ -373,8 +373,7 @@ class DeleteTag(ImportAction):
|
|
|
373
373
|
"""
|
|
374
374
|
|
|
375
375
|
def __str__(self) -> str:
|
|
376
|
-
|
|
377
|
-
return str(_("Delete tag (external_id={external_id})").format(external_id=taxonomy_tag.external_id))
|
|
376
|
+
return str(_("Delete tag (external_id={external_id})").format(external_id=self.tag.id))
|
|
378
377
|
|
|
379
378
|
name = "delete"
|
|
380
379
|
|
|
@@ -397,8 +396,11 @@ class DeleteTag(ImportAction):
|
|
|
397
396
|
"""
|
|
398
397
|
Delete a tag
|
|
399
398
|
"""
|
|
400
|
-
|
|
401
|
-
|
|
399
|
+
try:
|
|
400
|
+
taxonomy_tag = self._get_tag()
|
|
401
|
+
taxonomy_tag.delete()
|
|
402
|
+
except Tag.DoesNotExist:
|
|
403
|
+
pass # The tag may be already cascade deleted if the parent tag was deleted
|
|
402
404
|
|
|
403
405
|
|
|
404
406
|
class WithoutChanges(ImportAction):
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/import_export/api.py
RENAMED
|
@@ -115,7 +115,7 @@ def import_tags(
|
|
|
115
115
|
tag_import_plan.execute(task)
|
|
116
116
|
task.end_success()
|
|
117
117
|
return True
|
|
118
|
-
except Exception as exception:
|
|
118
|
+
except Exception as exception:
|
|
119
119
|
# Log any exception
|
|
120
120
|
task.log_exception(exception)
|
|
121
121
|
return False
|
|
@@ -93,14 +93,16 @@ class TagImportPlan:
|
|
|
93
93
|
# Verify if there is not a parent update before
|
|
94
94
|
if not self._search_parent_update(child.external_id, tag.external_id):
|
|
95
95
|
# Change parent to avoid delete childs
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
if child.external_id not in tags:
|
|
97
|
+
# Only update parent if the child is not going to be deleted
|
|
98
|
+
self._build_action(
|
|
99
|
+
UpdateParentTag,
|
|
100
|
+
TagItem(
|
|
101
|
+
id=child.external_id,
|
|
102
|
+
value=child.value,
|
|
103
|
+
parent_id=None,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
104
106
|
|
|
105
107
|
# Delete action
|
|
106
108
|
self._build_action(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/__init__.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/apps.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/urls.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/contrib/media_server/views.py
RENAMED
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/components/models.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/contents/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/model_mixins.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/core/publishing/models.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
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning/rest_api/v1/components.py
RENAMED
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_learning.egg-info/dependency_links.txt
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
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/__init__.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/models/utils.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/urls.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/utils.py
RENAMED
|
File without changes
|
{openedx-learning-0.3.7 → openedx-learning-0.4.0}/openedx_tagging/core/tagging/rest_api/v1/views.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|