openedx-learning 0.4.4__tar.gz → 0.5.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.
- {openedx-learning-0.4.4/openedx_learning.egg-info → openedx-learning-0.5.1}/PKG-INFO +1 -1
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/__init__.py +1 -1
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/admin.py +3 -4
- openedx-learning-0.5.1/openedx_learning/core/components/api.py +340 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/migrations/0001_initial.py +22 -7
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/models.py +78 -31
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/api.py +7 -5
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/migrations/0001_initial.py +3 -3
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/models.py +5 -5
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/admin.py +3 -1
- openedx-learning-0.5.1/openedx_learning/core/publishing/api.py +427 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/migrations/0001_initial.py +7 -6
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/model_mixins.py +98 -44
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/models.py +26 -3
- openedx-learning-0.5.1/openedx_learning/lib/managers.py +38 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1/openedx_learning.egg-info}/PKG-INFO +1 -1
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning.egg-info/SOURCES.txt +2 -1
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/api.py +8 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/api.py +1 -1
- openedx-learning-0.5.1/openedx_tagging/core/tagging/migrations/0015_taxonomy_export_id.py +38 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/models/base.py +23 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/serializers.py +3 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/views.py +19 -2
- openedx-learning-0.4.4/openedx_learning/core/components/api.py +0 -147
- openedx-learning-0.4.4/openedx_learning/core/publishing/api.py +0 -245
- openedx-learning-0.4.4/openedx_learning/core/publishing/migrations/0002_alter_fk_on_delete.py +0 -30
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/CHANGELOG.rst +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/LICENSE.txt +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/MANIFEST.in +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/README.rst +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/contrib/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/contrib/media_server/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/contrib/media_server/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/contrib/media_server/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/contrib/media_server/views.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/migrations/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/admin.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/contents/migrations/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/publishing/migrations/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/admin_utils.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/cache.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/collations.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/fields.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/test_utils.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/lib/validators.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/v1/components.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/rest_api/v1/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning.egg-info/dependency_links.txt +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning.egg-info/not-zip-safe +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning.egg-info/requires.txt +2 -2
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning.egg-info/top_level.txt +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/admin.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/apps.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/data.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/actions.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/exceptions.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/import_plan.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/parsers.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/tasks.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/template.csv +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/import_export/template.json +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0001_initial.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0001_squashed.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0002_auto_20230718_2026.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0003_auto_20230721_1238.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0004_auto_20230723_2001.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0005_language_taxonomy.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0006_alter_objecttag_unique_together.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0006_auto_20230802_1631.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0009_alter_objecttag_object_id.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0010_cleanups.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0011_remove_required.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0012_language_taxonomy.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0013_tag_parent_blank.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/0014_minor_fixes.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/migrations/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/models/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/models/import_export.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/models/system_defined.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/models/utils.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/paginators.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/utils.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/__init__.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/permissions.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rest_api/v1/views_import.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/rules.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_tagging/core/tagging/urls.py +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/requirements/base.in +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/setup.cfg +0 -0
- {openedx-learning-0.4.4 → openedx-learning-0.5.1}/setup.py +0 -0
|
@@ -35,16 +35,15 @@ class ComponentAdmin(ReadOnlyModelAdmin):
|
|
|
35
35
|
"""
|
|
36
36
|
Django admin configuration for Component
|
|
37
37
|
"""
|
|
38
|
-
list_display = ("key", "uuid", "
|
|
38
|
+
list_display = ("key", "uuid", "component_type", "created")
|
|
39
39
|
readonly_fields = [
|
|
40
40
|
"learning_package",
|
|
41
41
|
"uuid",
|
|
42
|
-
"
|
|
43
|
-
"type",
|
|
42
|
+
"component_type",
|
|
44
43
|
"key",
|
|
45
44
|
"created",
|
|
46
45
|
]
|
|
47
|
-
list_filter = ("
|
|
46
|
+
list_filter = ("component_type", "learning_package")
|
|
48
47
|
search_fields = ["publishable_entity__uuid", "publishable_entity__key"]
|
|
49
48
|
inlines = [ComponentVersionInline]
|
|
50
49
|
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Components API (warning: UNSTABLE, in progress API)
|
|
3
|
+
|
|
4
|
+
These functions are often going to be simple-looking write operations, but there
|
|
5
|
+
is bookkeeping logic needed across multiple models to keep state consistent. You
|
|
6
|
+
can read from the models directly for various queries if necessary–we do this in
|
|
7
|
+
the Django Admin for instance. But you should NEVER mutate this app's models
|
|
8
|
+
directly, since there might be other related models that you may not know about.
|
|
9
|
+
|
|
10
|
+
Please look at the models.py file for more information about the kinds of data
|
|
11
|
+
are stored in this app.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from django.db.models import Q, QuerySet
|
|
19
|
+
from django.db.transaction import atomic
|
|
20
|
+
|
|
21
|
+
from ...lib.cache import lru_cache
|
|
22
|
+
from ..publishing import api as publishing_api
|
|
23
|
+
from .models import Component, ComponentType, ComponentVersion, ComponentVersionRawContent
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=128)
|
|
27
|
+
def get_or_create_component_type_id(namespace: str, name: str) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Get the ID of a ComponentType, and create if missing.
|
|
30
|
+
"""
|
|
31
|
+
component_type, _created = ComponentType.objects.get_or_create(
|
|
32
|
+
namespace=namespace,
|
|
33
|
+
name=name,
|
|
34
|
+
)
|
|
35
|
+
return component_type.id
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_component(
|
|
39
|
+
learning_package_id: int,
|
|
40
|
+
/,
|
|
41
|
+
namespace: str,
|
|
42
|
+
type_name: str,
|
|
43
|
+
local_key: str,
|
|
44
|
+
created: datetime,
|
|
45
|
+
created_by: int | None,
|
|
46
|
+
) -> Component:
|
|
47
|
+
"""
|
|
48
|
+
Create a new Component (an entity like a Problem or Video)
|
|
49
|
+
"""
|
|
50
|
+
key = f"{namespace}:{type_name}@{local_key}"
|
|
51
|
+
with atomic():
|
|
52
|
+
publishable_entity = publishing_api.create_publishable_entity(
|
|
53
|
+
learning_package_id, key, created, created_by
|
|
54
|
+
)
|
|
55
|
+
component = Component.objects.create(
|
|
56
|
+
publishable_entity=publishable_entity,
|
|
57
|
+
learning_package_id=learning_package_id,
|
|
58
|
+
component_type_id=get_or_create_component_type_id(namespace, type_name),
|
|
59
|
+
local_key=local_key,
|
|
60
|
+
)
|
|
61
|
+
return component
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_component_version(
|
|
65
|
+
component_pk: int,
|
|
66
|
+
/,
|
|
67
|
+
version_num: int,
|
|
68
|
+
title: str,
|
|
69
|
+
created: datetime,
|
|
70
|
+
created_by: int | None,
|
|
71
|
+
) -> ComponentVersion:
|
|
72
|
+
"""
|
|
73
|
+
Create a new ComponentVersion
|
|
74
|
+
"""
|
|
75
|
+
with atomic():
|
|
76
|
+
publishable_entity_version = publishing_api.create_publishable_entity_version(
|
|
77
|
+
component_pk,
|
|
78
|
+
version_num=version_num,
|
|
79
|
+
title=title,
|
|
80
|
+
created=created,
|
|
81
|
+
created_by=created_by,
|
|
82
|
+
)
|
|
83
|
+
component_version = ComponentVersion.objects.create(
|
|
84
|
+
publishable_entity_version=publishable_entity_version,
|
|
85
|
+
component_id=component_pk,
|
|
86
|
+
)
|
|
87
|
+
return component_version
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_next_version(
|
|
91
|
+
component_pk: int,
|
|
92
|
+
/,
|
|
93
|
+
title: str,
|
|
94
|
+
content_to_replace: dict[str, int | None],
|
|
95
|
+
created: datetime,
|
|
96
|
+
created_by: int | None = None,
|
|
97
|
+
) -> ComponentVersion:
|
|
98
|
+
"""
|
|
99
|
+
Create a new ComponentVersion based on the most recent version.
|
|
100
|
+
|
|
101
|
+
A very common pattern for making a new ComponentVersion is going to be "make
|
|
102
|
+
it just like the last version, except changing these one or two things".
|
|
103
|
+
Before calling this, you should create any new contents via the contents
|
|
104
|
+
API, since ``content_to_replace`` needs RawContent IDs for the values.
|
|
105
|
+
|
|
106
|
+
The ``content_to_replace`` dict is a mapping of strings representing the
|
|
107
|
+
local path/key for a file, to ``RawContent.id`` values. Using a `None` for
|
|
108
|
+
a value in this dict means to delete that key in the next version.
|
|
109
|
+
|
|
110
|
+
It is okay to mark entries for deletion that don't exist. For instance, if a
|
|
111
|
+
version has ``a.txt`` and ``b.txt``, sending a ``content_to_replace`` value
|
|
112
|
+
of ``{"a.txt": None, "c.txt": None}`` will remove ``a.txt`` from the next
|
|
113
|
+
version, leave ``b.txt`` alone, and will not error–even though there is no
|
|
114
|
+
``c.txt`` in the previous version. This is to make it a little more
|
|
115
|
+
convenient to remove paths (e.g. due to deprecation) without having to
|
|
116
|
+
always check for its existence first.
|
|
117
|
+
|
|
118
|
+
TODO: Have to add learning_downloadable info to this when it comes time to
|
|
119
|
+
support static asset download.
|
|
120
|
+
"""
|
|
121
|
+
# This needs to grab the highest version_num for this Publishable Entity.
|
|
122
|
+
# This will often be the Draft version, but not always. For instance, if
|
|
123
|
+
# an entity was soft-deleted, the draft would be None, but the version_num
|
|
124
|
+
# should pick up from the last edited version. Likewise, a Draft might get
|
|
125
|
+
# reverted to an earlier version, but we want the latest version_num when
|
|
126
|
+
# creating the next version.
|
|
127
|
+
component = Component.objects.get(pk=component_pk)
|
|
128
|
+
last_version = component.versioning.latest
|
|
129
|
+
if last_version is None:
|
|
130
|
+
next_version_num = 1
|
|
131
|
+
else:
|
|
132
|
+
next_version_num = last_version.version_num + 1
|
|
133
|
+
|
|
134
|
+
with atomic():
|
|
135
|
+
publishable_entity_version = publishing_api.create_publishable_entity_version(
|
|
136
|
+
component_pk,
|
|
137
|
+
version_num=next_version_num,
|
|
138
|
+
title=title,
|
|
139
|
+
created=created,
|
|
140
|
+
created_by=created_by,
|
|
141
|
+
)
|
|
142
|
+
component_version = ComponentVersion.objects.create(
|
|
143
|
+
publishable_entity_version=publishable_entity_version,
|
|
144
|
+
component_id=component_pk,
|
|
145
|
+
)
|
|
146
|
+
# First copy the new stuff over...
|
|
147
|
+
for key, raw_content_pk in content_to_replace.items():
|
|
148
|
+
# If the raw_content_pk is None, it means we want to remove the
|
|
149
|
+
# content represented by our key from the next version. Otherwise,
|
|
150
|
+
# we add our key->raw_content_pk mapping to the next version.
|
|
151
|
+
if raw_content_pk is not None:
|
|
152
|
+
ComponentVersionRawContent.objects.create(
|
|
153
|
+
raw_content_id=raw_content_pk,
|
|
154
|
+
component_version=component_version,
|
|
155
|
+
key=key,
|
|
156
|
+
learner_downloadable=False,
|
|
157
|
+
)
|
|
158
|
+
# Now copy any old associations that existed, as long as they aren't
|
|
159
|
+
# in conflict with the new stuff or marked for deletion.
|
|
160
|
+
last_version_content_mapping = ComponentVersionRawContent.objects \
|
|
161
|
+
.filter(component_version=last_version)
|
|
162
|
+
for cvrc in last_version_content_mapping:
|
|
163
|
+
if cvrc.key not in content_to_replace:
|
|
164
|
+
ComponentVersionRawContent.objects.create(
|
|
165
|
+
raw_content_id=cvrc.raw_content_id,
|
|
166
|
+
component_version=component_version,
|
|
167
|
+
key=cvrc.key,
|
|
168
|
+
learner_downloadable=cvrc.learner_downloadable,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return component_version
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_component_and_version(
|
|
175
|
+
learning_package_id: int,
|
|
176
|
+
/,
|
|
177
|
+
namespace: str,
|
|
178
|
+
type_name: str,
|
|
179
|
+
local_key: str,
|
|
180
|
+
title: str,
|
|
181
|
+
created: datetime,
|
|
182
|
+
created_by: int | None = None,
|
|
183
|
+
) -> tuple[Component, ComponentVersion]:
|
|
184
|
+
"""
|
|
185
|
+
Create a Component and associated ComponentVersion atomically
|
|
186
|
+
"""
|
|
187
|
+
with atomic():
|
|
188
|
+
component = create_component(
|
|
189
|
+
learning_package_id, namespace, type_name, local_key, created, created_by
|
|
190
|
+
)
|
|
191
|
+
component_version = create_component_version(
|
|
192
|
+
component.pk,
|
|
193
|
+
version_num=1,
|
|
194
|
+
title=title,
|
|
195
|
+
created=created,
|
|
196
|
+
created_by=created_by,
|
|
197
|
+
)
|
|
198
|
+
return (component, component_version)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_component(component_pk: int, /) -> Component:
|
|
202
|
+
"""
|
|
203
|
+
Get Component by its primary key.
|
|
204
|
+
|
|
205
|
+
This is the same as the PublishableEntity's ID primary key.
|
|
206
|
+
"""
|
|
207
|
+
return Component.with_publishing_relations.get(pk=component_pk)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_component_by_key(
|
|
211
|
+
learning_package_id: int,
|
|
212
|
+
/,
|
|
213
|
+
namespace: str,
|
|
214
|
+
type_name: str,
|
|
215
|
+
local_key: str,
|
|
216
|
+
) -> Component:
|
|
217
|
+
"""
|
|
218
|
+
Get a Component by its unique (namespace, type, local_key) tuple.
|
|
219
|
+
"""
|
|
220
|
+
return Component.with_publishing_relations \
|
|
221
|
+
.get(
|
|
222
|
+
learning_package_id=learning_package_id,
|
|
223
|
+
component_type__namespace=namespace,
|
|
224
|
+
component_type__name=type_name,
|
|
225
|
+
local_key=local_key,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def component_exists_by_key(
|
|
230
|
+
learning_package_id: int,
|
|
231
|
+
/,
|
|
232
|
+
namespace: str,
|
|
233
|
+
type_name: str,
|
|
234
|
+
local_key: str
|
|
235
|
+
) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Return True/False for whether a Component exists.
|
|
238
|
+
|
|
239
|
+
Note that a Component still exists even if it's been soft-deleted (there's
|
|
240
|
+
no current Draft version for it), or if it's been unpublished.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
_component = Component.objects.only('pk', 'component_type').get(
|
|
244
|
+
learning_package_id=learning_package_id,
|
|
245
|
+
component_type__namespace=namespace,
|
|
246
|
+
component_type__name=type_name,
|
|
247
|
+
local_key=local_key,
|
|
248
|
+
)
|
|
249
|
+
return True
|
|
250
|
+
except Component.DoesNotExist:
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_components(
|
|
255
|
+
learning_package_id: int,
|
|
256
|
+
/,
|
|
257
|
+
draft: bool | None = None,
|
|
258
|
+
published: bool | None = None,
|
|
259
|
+
namespace: str | None = None,
|
|
260
|
+
type_names: list[str] | None = None,
|
|
261
|
+
draft_title: str | None = None,
|
|
262
|
+
published_title: str | None = None,
|
|
263
|
+
) -> QuerySet[Component]:
|
|
264
|
+
"""
|
|
265
|
+
Fetch a QuerySet of Components for a LearningPackage using various filters.
|
|
266
|
+
|
|
267
|
+
This method will pre-load all the relations that we need in order to get
|
|
268
|
+
info from the Component's draft and published versions, since we'll be
|
|
269
|
+
referencing these a lot.
|
|
270
|
+
"""
|
|
271
|
+
qset = Component.with_publishing_relations \
|
|
272
|
+
.filter(learning_package_id=learning_package_id) \
|
|
273
|
+
.order_by('pk')
|
|
274
|
+
|
|
275
|
+
if draft is not None:
|
|
276
|
+
qset = qset.filter(publishable_entity__draft__version__isnull=not draft)
|
|
277
|
+
if published is not None:
|
|
278
|
+
qset = qset.filter(publishable_entity__published__version__isnull=not published)
|
|
279
|
+
if namespace is not None:
|
|
280
|
+
qset = qset.filter(component_type__namespace=namespace)
|
|
281
|
+
if type_names is not None:
|
|
282
|
+
qset = qset.filter(component_type__name__in=type_names)
|
|
283
|
+
if draft_title is not None:
|
|
284
|
+
qset = qset.filter(
|
|
285
|
+
publishable_entity__draft__version__title__icontains=draft_title
|
|
286
|
+
)
|
|
287
|
+
if published_title is not None:
|
|
288
|
+
qset = qset.filter(
|
|
289
|
+
publishable_entity__published__version__title__icontains=published_title
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return qset
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_component_version_content(
|
|
296
|
+
learning_package_key: str,
|
|
297
|
+
component_key: str,
|
|
298
|
+
version_num: int,
|
|
299
|
+
key: Path,
|
|
300
|
+
) -> ComponentVersionRawContent:
|
|
301
|
+
"""
|
|
302
|
+
Look up ComponentVersionRawContent by human readable keys.
|
|
303
|
+
|
|
304
|
+
Can raise a django.core.exceptions.ObjectDoesNotExist error if there is no
|
|
305
|
+
matching ComponentVersionRawContent.
|
|
306
|
+
"""
|
|
307
|
+
queries = (
|
|
308
|
+
Q(component_version__component__learning_package__key=learning_package_key)
|
|
309
|
+
& Q(component_version__component__publishable_entity__key=component_key)
|
|
310
|
+
& Q(component_version__publishable_entity_version__version_num=version_num)
|
|
311
|
+
& Q(key=key)
|
|
312
|
+
)
|
|
313
|
+
return ComponentVersionRawContent.objects \
|
|
314
|
+
.select_related(
|
|
315
|
+
"raw_content",
|
|
316
|
+
"raw_content__media_type",
|
|
317
|
+
"raw_content__textcontent",
|
|
318
|
+
"component_version",
|
|
319
|
+
"component_version__component",
|
|
320
|
+
"component_version__component__learning_package",
|
|
321
|
+
).get(queries)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def add_content_to_component_version(
|
|
325
|
+
component_version_id: int,
|
|
326
|
+
/,
|
|
327
|
+
raw_content_id: int,
|
|
328
|
+
key: str,
|
|
329
|
+
learner_downloadable=False,
|
|
330
|
+
) -> ComponentVersionRawContent:
|
|
331
|
+
"""
|
|
332
|
+
Add a RawContent to the given ComponentVersion
|
|
333
|
+
"""
|
|
334
|
+
cvrc, _created = ComponentVersionRawContent.objects.get_or_create(
|
|
335
|
+
component_version_id=component_version_id,
|
|
336
|
+
raw_content_id=raw_content_id,
|
|
337
|
+
key=key,
|
|
338
|
+
learner_downloadable=learner_downloadable,
|
|
339
|
+
)
|
|
340
|
+
return cvrc
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 3.2.23 on
|
|
1
|
+
# Generated by Django 3.2.23 on 2024-01-31 05:34
|
|
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', '0001_initial'),
|
|
17
17
|
('oel_contents', '0001_initial'),
|
|
18
18
|
]
|
|
19
19
|
|
|
@@ -22,16 +22,21 @@ class Migration(migrations.Migration):
|
|
|
22
22
|
name='Component',
|
|
23
23
|
fields=[
|
|
24
24
|
('publishable_entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentity')),
|
|
25
|
-
('namespace', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
|
|
26
|
-
('type', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
|
|
27
25
|
('local_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500)),
|
|
28
|
-
('learning_package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage')),
|
|
29
26
|
],
|
|
30
27
|
options={
|
|
31
28
|
'verbose_name': 'Component',
|
|
32
29
|
'verbose_name_plural': 'Components',
|
|
33
30
|
},
|
|
34
31
|
),
|
|
32
|
+
migrations.CreateModel(
|
|
33
|
+
name='ComponentType',
|
|
34
|
+
fields=[
|
|
35
|
+
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
36
|
+
('namespace', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
|
|
37
|
+
('name', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
|
|
38
|
+
],
|
|
39
|
+
),
|
|
35
40
|
migrations.CreateModel(
|
|
36
41
|
name='ComponentVersion',
|
|
37
42
|
fields=[
|
|
@@ -59,6 +64,16 @@ class Migration(migrations.Migration):
|
|
|
59
64
|
name='raw_contents',
|
|
60
65
|
field=models.ManyToManyField(related_name='component_versions', through='oel_components.ComponentVersionRawContent', to='oel_contents.RawContent'),
|
|
61
66
|
),
|
|
67
|
+
migrations.AddField(
|
|
68
|
+
model_name='component',
|
|
69
|
+
name='component_type',
|
|
70
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='oel_components.componenttype'),
|
|
71
|
+
),
|
|
72
|
+
migrations.AddField(
|
|
73
|
+
model_name='component',
|
|
74
|
+
name='learning_package',
|
|
75
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage'),
|
|
76
|
+
),
|
|
62
77
|
migrations.AddIndex(
|
|
63
78
|
model_name='componentversionrawcontent',
|
|
64
79
|
index=models.Index(fields=['raw_content', 'component_version'], name='oel_cvrawcontent_c_cv'),
|
|
@@ -73,10 +88,10 @@ class Migration(migrations.Migration):
|
|
|
73
88
|
),
|
|
74
89
|
migrations.AddIndex(
|
|
75
90
|
model_name='component',
|
|
76
|
-
index=models.Index(fields=['
|
|
91
|
+
index=models.Index(fields=['component_type', 'local_key'], name='oel_component_idx_ct_lk'),
|
|
77
92
|
),
|
|
78
93
|
migrations.AddConstraint(
|
|
79
94
|
model_name='component',
|
|
80
|
-
constraint=models.UniqueConstraint(fields=('learning_package', '
|
|
95
|
+
constraint=models.UniqueConstraint(fields=('learning_package', 'component_type', 'local_key'), name='oel_component_uniq_lc_ct_lk'),
|
|
81
96
|
),
|
|
82
97
|
]
|
{openedx-learning-0.4.4 → openedx-learning-0.5.1}/openedx_learning/core/components/models.py
RENAMED
|
@@ -21,12 +21,52 @@ from __future__ import annotations
|
|
|
21
21
|
from django.db import models
|
|
22
22
|
|
|
23
23
|
from openedx_learning.lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field
|
|
24
|
+
from openedx_learning.lib.managers import WithRelationsManager
|
|
24
25
|
|
|
25
26
|
from ..contents.models import RawContent
|
|
26
27
|
from ..publishing.model_mixins import PublishableEntityMixin, PublishableEntityVersionMixin
|
|
27
28
|
from ..publishing.models import LearningPackage
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
class ComponentType(models.Model):
|
|
32
|
+
"""
|
|
33
|
+
Normalized representation of a type of Component.
|
|
34
|
+
|
|
35
|
+
The only namespace being used initially will be 'xblock.v1', but we will
|
|
36
|
+
probably add a few others over time, such as a component type to represent
|
|
37
|
+
packages of files for things like Files and Uploads or python_lib.zip files.
|
|
38
|
+
|
|
39
|
+
Make a ForeignKey against this table if you have to set policy based on the
|
|
40
|
+
type of Components–e.g. marking certain types of XBlocks as approved vs.
|
|
41
|
+
experimental for use in libraries.
|
|
42
|
+
"""
|
|
43
|
+
id = models.AutoField(primary_key=True)
|
|
44
|
+
|
|
45
|
+
# namespace and name work together to help figure out what Component needs
|
|
46
|
+
# to handle this data. A namespace is *required*. The namespace for XBlocks
|
|
47
|
+
# is "xblock.v1" (to match the setup.py entrypoint naming scheme).
|
|
48
|
+
namespace = case_sensitive_char_field(max_length=100, blank=False)
|
|
49
|
+
|
|
50
|
+
# name is a way to help sub-divide namespace if that's convenient. This
|
|
51
|
+
# field cannot be null, but it can be blank if it's not necessary. For an
|
|
52
|
+
# XBlock, this corresponds to tag, e.g. "video". It's also the block_type in
|
|
53
|
+
# the UsageKey.
|
|
54
|
+
name = case_sensitive_char_field(max_length=100, blank=True)
|
|
55
|
+
|
|
56
|
+
constraints = [
|
|
57
|
+
models.UniqueConstraint(
|
|
58
|
+
fields=[
|
|
59
|
+
"namespace",
|
|
60
|
+
"name",
|
|
61
|
+
],
|
|
62
|
+
name="oel_component_type_uniq_ns_n",
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return f"{self.namespace}:{self.name}"
|
|
68
|
+
|
|
69
|
+
|
|
30
70
|
class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
|
|
31
71
|
"""
|
|
32
72
|
This represents any Component that has ever existed in a LearningPackage.
|
|
@@ -44,6 +84,12 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
|
|
|
44
84
|
|
|
45
85
|
A Component belongs to exactly one LearningPackage.
|
|
46
86
|
|
|
87
|
+
A Component is 1:1 with PublishableEntity and has matching primary key
|
|
88
|
+
values. More specifically, ``Component.pk`` maps to
|
|
89
|
+
``Component.publishable_entity_id``, and any place where the Publishing API
|
|
90
|
+
module expects to get a ``PublishableEntity.id``, you can use a
|
|
91
|
+
``Component.pk`` instead.
|
|
92
|
+
|
|
47
93
|
Identifiers
|
|
48
94
|
-----------
|
|
49
95
|
|
|
@@ -56,7 +102,7 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
|
|
|
56
102
|
-----------------
|
|
57
103
|
|
|
58
104
|
The ``key`` field on Component's ``publishable_entity`` is dervied from the
|
|
59
|
-
``
|
|
105
|
+
``component_type`` and ``local_key`` fields in this model. We don't support
|
|
60
106
|
changing the keys yet, but if we do, those values need to be kept in sync.
|
|
61
107
|
|
|
62
108
|
How build on this model
|
|
@@ -68,63 +114,64 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
|
|
|
68
114
|
# Tell mypy what type our objects manager has.
|
|
69
115
|
# It's actually PublishableEntityMixinManager, but that has the exact same
|
|
70
116
|
# interface as the base manager class.
|
|
71
|
-
objects: models.Manager[Component]
|
|
117
|
+
objects: models.Manager[Component] = WithRelationsManager(
|
|
118
|
+
'component_type'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
with_publishing_relations: models.Manager[Component] = WithRelationsManager(
|
|
122
|
+
'component_type',
|
|
123
|
+
'publishable_entity',
|
|
124
|
+
'publishable_entity__draft__version',
|
|
125
|
+
'publishable_entity__draft__version__componentversion',
|
|
126
|
+
'publishable_entity__published__version',
|
|
127
|
+
'publishable_entity__published__version__componentversion',
|
|
128
|
+
)
|
|
72
129
|
|
|
73
130
|
# This foreign key is technically redundant because we're already locked to
|
|
74
|
-
# a single LearningPackage through our publishable_entity relation. However,
|
|
75
|
-
# this foreign key directly allows us to make indexes that efficiently
|
|
131
|
+
# a single LearningPackage through our publishable_entity relation. However,
|
|
132
|
+
# having this foreign key directly allows us to make indexes that efficiently
|
|
76
133
|
# query by other Component fields within a given LearningPackage, which is
|
|
77
134
|
# going to be a common use case (and we can't make a compound index using
|
|
78
135
|
# columns from different tables).
|
|
79
136
|
learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
|
|
80
137
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
namespace = case_sensitive_char_field(max_length=100, blank=False)
|
|
85
|
-
|
|
86
|
-
# type is a way to help sub-divide namespace if that's convenient. This
|
|
87
|
-
# field cannot be null, but it can be blank if it's not necessary. For an
|
|
88
|
-
# XBlock, type corresponds to tag, e.g. "video". It's also the block_type in
|
|
89
|
-
# the UsageKey.
|
|
90
|
-
type = case_sensitive_char_field(max_length=100, blank=True)
|
|
138
|
+
# What kind of Component are we? This will usually represent a specific
|
|
139
|
+
# XBlock block_type, but we want it to be more flexible in the long term.
|
|
140
|
+
component_type = models.ForeignKey(ComponentType, on_delete=models.PROTECT)
|
|
91
141
|
|
|
92
|
-
# local_key is an identifier that is local to the
|
|
93
|
-
# publishable.key should be calculated as a
|
|
94
|
-
# local_key
|
|
142
|
+
# local_key is an identifier that is local to the learning_package and
|
|
143
|
+
# component_type. The publishable.key should be calculated as a
|
|
144
|
+
# combination of component_type and local_key.
|
|
95
145
|
local_key = key_field()
|
|
96
146
|
|
|
97
147
|
class Meta:
|
|
98
148
|
constraints = [
|
|
99
|
-
# The combination of (
|
|
149
|
+
# The combination of (component_type, local_key) is unique within
|
|
100
150
|
# a given LearningPackage. Note that this means it is possible to
|
|
101
|
-
# have two Components
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
151
|
+
# have two Components in the same LearningPackage to have the same
|
|
152
|
+
# local_key if the component_types are different. So for example,
|
|
153
|
+
# you could have a ProblemBlock and VideoBlock that both have the
|
|
154
|
+
# local_key "week_1".
|
|
105
155
|
models.UniqueConstraint(
|
|
106
156
|
fields=[
|
|
107
157
|
"learning_package",
|
|
108
|
-
"
|
|
109
|
-
"type",
|
|
158
|
+
"component_type",
|
|
110
159
|
"local_key",
|
|
111
160
|
],
|
|
112
|
-
name="
|
|
161
|
+
name="oel_component_uniq_lc_ct_lk",
|
|
113
162
|
),
|
|
114
163
|
]
|
|
115
164
|
indexes = [
|
|
116
|
-
# Global
|
|
165
|
+
# Global Component-Type/Local-Key Index:
|
|
117
166
|
# * Search by the different Components fields across all Learning
|
|
118
167
|
# Packages on the site. This would be a support-oriented tool
|
|
119
168
|
# from Django Admin.
|
|
120
169
|
models.Index(
|
|
121
170
|
fields=[
|
|
122
|
-
"
|
|
123
|
-
"namespace",
|
|
124
|
-
"type",
|
|
171
|
+
"component_type",
|
|
125
172
|
"local_key",
|
|
126
173
|
],
|
|
127
|
-
name="
|
|
174
|
+
name="oel_component_idx_ct_lk",
|
|
128
175
|
),
|
|
129
176
|
]
|
|
130
177
|
|
|
@@ -133,7 +180,7 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
|
|
|
133
180
|
verbose_name_plural = "Components"
|
|
134
181
|
|
|
135
182
|
def __str__(self):
|
|
136
|
-
return f"{self.namespace}:{self.
|
|
183
|
+
return f"{self.component_type.namespace}:{self.component_type.name}:{self.local_key}"
|
|
137
184
|
|
|
138
185
|
|
|
139
186
|
class ComponentVersion(PublishableEntityVersionMixin):
|
|
@@ -12,14 +12,14 @@ 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
|
|
16
|
-
from
|
|
17
|
-
|
|
15
|
+
from ...lib.cache import lru_cache
|
|
16
|
+
from ...lib.fields import create_hash_digest
|
|
18
17
|
from .models import MediaType, RawContent, TextContent
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
def create_raw_content(
|
|
22
21
|
learning_package_id: int,
|
|
22
|
+
/,
|
|
23
23
|
data_bytes: bytes,
|
|
24
24
|
mime_type: str,
|
|
25
25
|
created: datetime,
|
|
@@ -32,7 +32,7 @@ def create_raw_content(
|
|
|
32
32
|
|
|
33
33
|
raw_content = RawContent.objects.create(
|
|
34
34
|
learning_package_id=learning_package_id,
|
|
35
|
-
media_type_id=
|
|
35
|
+
media_type_id=get_or_create_media_type_id(mime_type),
|
|
36
36
|
hash_digest=hash_digest,
|
|
37
37
|
size=len(data_bytes),
|
|
38
38
|
created=created,
|
|
@@ -57,7 +57,7 @@ def create_text_from_raw_content(raw_content: RawContent, encoding="utf-8-sig")
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
@lru_cache(maxsize=128)
|
|
60
|
-
def
|
|
60
|
+
def get_or_create_media_type_id(mime_type: str) -> int:
|
|
61
61
|
"""
|
|
62
62
|
Return the MediaType.id for the desired mime_type string.
|
|
63
63
|
|
|
@@ -91,6 +91,7 @@ def get_media_type_id(mime_type: str) -> int:
|
|
|
91
91
|
|
|
92
92
|
def get_or_create_raw_content(
|
|
93
93
|
learning_package_id: int,
|
|
94
|
+
/,
|
|
94
95
|
data_bytes: bytes,
|
|
95
96
|
mime_type: str,
|
|
96
97
|
created: datetime,
|
|
@@ -117,6 +118,7 @@ def get_or_create_raw_content(
|
|
|
117
118
|
|
|
118
119
|
def get_or_create_text_content_from_bytes(
|
|
119
120
|
learning_package_id: int,
|
|
121
|
+
/,
|
|
120
122
|
data_bytes: bytes,
|
|
121
123
|
mime_type: str,
|
|
122
124
|
created: datetime,
|