openedx-learning 0.5.1__py2.py3-none-any.whl → 0.6.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openedx_learning/__init__.py +1 -1
- openedx_learning/contrib/media_server/views.py +2 -2
- openedx_learning/core/components/admin.py +22 -31
- openedx_learning/core/components/api.py +51 -47
- openedx_learning/core/components/migrations/0001_initial.py +12 -12
- openedx_learning/core/components/migrations/0002_alter_componentversioncontent_key.py +20 -0
- openedx_learning/core/components/models.py +37 -30
- openedx_learning/core/contents/admin.py +13 -20
- openedx_learning/core/contents/api.py +104 -94
- openedx_learning/core/contents/migrations/0001_initial.py +23 -30
- openedx_learning/core/contents/models.py +230 -149
- openedx_learning/core/publishing/migrations/0001_initial.py +2 -2
- openedx_learning/core/publishing/migrations/0002_alter_learningpackage_key_and_more.py +25 -0
- openedx_learning/core/publishing/models.py +41 -2
- openedx_learning/lib/fields.py +14 -2
- openedx_learning/lib/managers.py +6 -2
- {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/METADATA +4 -4
- {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/RECORD +24 -22
- openedx_tagging/core/tagging/data.py +1 -0
- openedx_tagging/core/tagging/models/base.py +36 -5
- openedx_tagging/core/tagging/rest_api/v1/serializers.py +1 -0
- {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/LICENSE.txt +0 -0
- {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/WHEEL +0 -0
- {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -6,58 +6,16 @@ are stored in this app.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import codecs
|
|
10
9
|
from datetime import datetime
|
|
11
10
|
|
|
12
11
|
from django.core.files.base import ContentFile
|
|
13
12
|
from django.db.transaction import atomic
|
|
14
13
|
|
|
15
|
-
from ...lib.cache import lru_cache
|
|
16
14
|
from ...lib.fields import create_hash_digest
|
|
17
|
-
from .models import
|
|
15
|
+
from .models import Content, MediaType
|
|
18
16
|
|
|
19
17
|
|
|
20
|
-
def
|
|
21
|
-
learning_package_id: int,
|
|
22
|
-
/,
|
|
23
|
-
data_bytes: bytes,
|
|
24
|
-
mime_type: str,
|
|
25
|
-
created: datetime,
|
|
26
|
-
hash_digest: str | None = None,
|
|
27
|
-
) -> RawContent:
|
|
28
|
-
"""
|
|
29
|
-
Create a new RawContent instance and persist it to storage.
|
|
30
|
-
"""
|
|
31
|
-
hash_digest = hash_digest or create_hash_digest(data_bytes)
|
|
32
|
-
|
|
33
|
-
raw_content = RawContent.objects.create(
|
|
34
|
-
learning_package_id=learning_package_id,
|
|
35
|
-
media_type_id=get_or_create_media_type_id(mime_type),
|
|
36
|
-
hash_digest=hash_digest,
|
|
37
|
-
size=len(data_bytes),
|
|
38
|
-
created=created,
|
|
39
|
-
)
|
|
40
|
-
raw_content.file.save(
|
|
41
|
-
f"{raw_content.learning_package.uuid}/{hash_digest}",
|
|
42
|
-
ContentFile(data_bytes),
|
|
43
|
-
)
|
|
44
|
-
return raw_content
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def create_text_from_raw_content(raw_content: RawContent, encoding="utf-8-sig") -> TextContent:
|
|
48
|
-
"""
|
|
49
|
-
Create a new TextContent instance for the given RawContent.
|
|
50
|
-
"""
|
|
51
|
-
text = codecs.decode(raw_content.file.open().read(), encoding)
|
|
52
|
-
return TextContent.objects.create(
|
|
53
|
-
raw_content=raw_content,
|
|
54
|
-
text=text,
|
|
55
|
-
length=len(text),
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@lru_cache(maxsize=128)
|
|
60
|
-
def get_or_create_media_type_id(mime_type: str) -> int:
|
|
18
|
+
def get_or_create_media_type(mime_type: str) -> MediaType:
|
|
61
19
|
"""
|
|
62
20
|
Return the MediaType.id for the desired mime_type string.
|
|
63
21
|
|
|
@@ -68,10 +26,12 @@ def get_or_create_media_type_id(mime_type: str) -> int:
|
|
|
68
26
|
the different XBlocks that will be installed in different server instances,
|
|
69
27
|
each of which will use their own MediaType.
|
|
70
28
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
29
|
+
Caching Warning: Be careful about putting any caching decorator around this
|
|
30
|
+
function (e.g. ``lru_cache``). It's possible that incorrect cache values
|
|
31
|
+
could leak out in the event of a rollback–e.g. new types are introduced in
|
|
32
|
+
a large import transaction which later fails. You can safely cache the
|
|
33
|
+
results that come back from this function with a local dict in your import
|
|
34
|
+
process instead.
|
|
75
35
|
"""
|
|
76
36
|
if "+" in mime_type:
|
|
77
37
|
base, suffix = mime_type.split("+")
|
|
@@ -80,69 +40,119 @@ def get_or_create_media_type_id(mime_type: str) -> int:
|
|
|
80
40
|
suffix = ""
|
|
81
41
|
|
|
82
42
|
main_type, sub_type = base.split("/")
|
|
83
|
-
|
|
43
|
+
media_type, _created = MediaType.objects.get_or_create(
|
|
84
44
|
type=main_type,
|
|
85
45
|
sub_type=sub_type,
|
|
86
46
|
suffix=suffix,
|
|
87
47
|
)
|
|
88
48
|
|
|
89
|
-
return
|
|
49
|
+
return media_type
|
|
90
50
|
|
|
91
51
|
|
|
92
|
-
def
|
|
52
|
+
def get_content(content_id: int, /) -> Content:
|
|
53
|
+
"""
|
|
54
|
+
Get a single Content object by its ID.
|
|
55
|
+
|
|
56
|
+
Content is always attached to something when it's created, like to a
|
|
57
|
+
ComponentVersion. That means the "right" way to access a Content is almost
|
|
58
|
+
always going to be via those relations and not via this function. But I
|
|
59
|
+
include this function anyway because it's tiny to write and it's better than
|
|
60
|
+
someone using a get_or_create_* function when they really just want to get.
|
|
61
|
+
"""
|
|
62
|
+
return Content.objects.get(id=content_id)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_or_create_text_content(
|
|
93
66
|
learning_package_id: int,
|
|
67
|
+
media_type_id: int,
|
|
94
68
|
/,
|
|
95
|
-
|
|
96
|
-
mime_type: str,
|
|
69
|
+
text: str,
|
|
97
70
|
created: datetime,
|
|
98
|
-
|
|
99
|
-
) ->
|
|
71
|
+
create_file: bool = False,
|
|
72
|
+
) -> Content:
|
|
100
73
|
"""
|
|
101
|
-
Get
|
|
102
|
-
|
|
74
|
+
Get or create a Content entry with text data stored in the database.
|
|
75
|
+
|
|
76
|
+
Use this when you want to create relatively small chunks of text that need
|
|
77
|
+
to be accessed quickly, especially if you're pulling back multiple rows at
|
|
78
|
+
once. For example, this is the function to call when storing OLX for a
|
|
79
|
+
component XBlock like a ProblemBlock.
|
|
80
|
+
|
|
81
|
+
This function will *always* create a text entry in the database. In addition
|
|
82
|
+
to this, if you specify ``create_file=True``, it will also save a copy of
|
|
83
|
+
that text data to the file storage backend. This is useful if we want to let
|
|
84
|
+
that file be downloadable by browsers in the LMS at some point.
|
|
85
|
+
|
|
86
|
+
If you want to create a large text file, or want to create a text file that
|
|
87
|
+
doesn't need to be stored in the database, call ``create_file_content``
|
|
88
|
+
instead of this function.
|
|
103
89
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
90
|
+
text_as_bytes = text.encode('utf-8')
|
|
91
|
+
hash_digest = create_hash_digest(text_as_bytes)
|
|
92
|
+
|
|
93
|
+
with atomic():
|
|
94
|
+
try:
|
|
95
|
+
content = Content.objects.get(
|
|
96
|
+
learning_package_id=learning_package_id,
|
|
97
|
+
media_type_id=media_type_id,
|
|
98
|
+
hash_digest=hash_digest,
|
|
99
|
+
)
|
|
100
|
+
except Content.DoesNotExist:
|
|
101
|
+
content = Content(
|
|
102
|
+
learning_package_id=learning_package_id,
|
|
103
|
+
media_type_id=media_type_id,
|
|
104
|
+
hash_digest=hash_digest,
|
|
105
|
+
created=created,
|
|
106
|
+
size=len(text_as_bytes),
|
|
107
|
+
text=text,
|
|
108
|
+
has_file=create_file,
|
|
109
|
+
)
|
|
110
|
+
content.full_clean()
|
|
111
|
+
content.save()
|
|
112
|
+
|
|
113
|
+
if create_file:
|
|
114
|
+
content.write_file(ContentFile(text_as_bytes))
|
|
115
|
+
|
|
116
|
+
return content
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_or_create_file_content(
|
|
120
120
|
learning_package_id: int,
|
|
121
|
+
media_type_id: int,
|
|
121
122
|
/,
|
|
122
|
-
|
|
123
|
-
mime_type: str,
|
|
123
|
+
data: bytes,
|
|
124
124
|
created: datetime,
|
|
125
|
-
|
|
126
|
-
encoding: str = "utf-8-sig",
|
|
127
|
-
):
|
|
125
|
+
) -> Content:
|
|
128
126
|
"""
|
|
129
|
-
Get
|
|
130
|
-
|
|
127
|
+
Get or create a Content with data stored in a file storage backend.
|
|
128
|
+
|
|
129
|
+
Use this function to store non-text data, large data, or data where low
|
|
130
|
+
latency access is not necessary. Also use this function (or
|
|
131
|
+
``get_or_create_text_content`` with ``create_file=True``) to store any
|
|
132
|
+
Content that you want to be downloadable by browsers in the LMS, since the
|
|
133
|
+
static asset serving system will only work with file-backed Content.
|
|
131
134
|
"""
|
|
135
|
+
hash_digest = create_hash_digest(data)
|
|
132
136
|
with atomic():
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
text_content = TextContent.objects.create(
|
|
139
|
-
raw_content=raw_content,
|
|
140
|
-
text=text,
|
|
141
|
-
length=len(text),
|
|
137
|
+
try:
|
|
138
|
+
content = Content.objects.get(
|
|
139
|
+
learning_package_id=learning_package_id,
|
|
140
|
+
media_type_id=media_type_id,
|
|
141
|
+
hash_digest=hash_digest,
|
|
142
142
|
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
except Content.DoesNotExist:
|
|
144
|
+
content = Content(
|
|
145
|
+
learning_package_id=learning_package_id,
|
|
146
|
+
media_type_id=media_type_id,
|
|
147
|
+
hash_digest=hash_digest,
|
|
148
|
+
created=created,
|
|
149
|
+
size=len(data),
|
|
150
|
+
text=None,
|
|
151
|
+
has_file=True,
|
|
152
|
+
)
|
|
153
|
+
content.full_clean()
|
|
154
|
+
content.save()
|
|
155
|
+
|
|
156
|
+
content.write_file(ContentFile(data))
|
|
147
157
|
|
|
148
|
-
return
|
|
158
|
+
return content
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Generated by Django 3.2.23 on 2024-
|
|
1
|
+
# Generated by Django 3.2.23 on 2024-02-06 03:23
|
|
2
2
|
|
|
3
3
|
import django.core.validators
|
|
4
4
|
import django.db.models.deletion
|
|
@@ -18,56 +18,49 @@ class Migration(migrations.Migration):
|
|
|
18
18
|
|
|
19
19
|
operations = [
|
|
20
20
|
migrations.CreateModel(
|
|
21
|
-
name='
|
|
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
|
-
),
|
|
29
|
-
migrations.CreateModel(
|
|
30
|
-
name='RawContent',
|
|
21
|
+
name='Content',
|
|
31
22
|
fields=[
|
|
32
23
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
33
|
-
('hash_digest', models.CharField(editable=False, max_length=40)),
|
|
34
24
|
('size', models.PositiveBigIntegerField(validators=[django.core.validators.MaxValueValidator(50000000)])),
|
|
25
|
+
('hash_digest', models.CharField(editable=False, max_length=40)),
|
|
26
|
+
('has_file', models.BooleanField()),
|
|
27
|
+
('text', openedx_learning.lib.fields.MultiCollationTextField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=50000, null=True)),
|
|
35
28
|
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
|
|
36
|
-
('file', models.FileField(null=True, upload_to='')),
|
|
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')),
|
|
39
29
|
],
|
|
40
30
|
options={
|
|
41
|
-
'verbose_name': '
|
|
42
|
-
'verbose_name_plural': '
|
|
31
|
+
'verbose_name': 'Content',
|
|
32
|
+
'verbose_name_plural': 'Contents',
|
|
43
33
|
},
|
|
44
34
|
),
|
|
45
35
|
migrations.CreateModel(
|
|
46
|
-
name='
|
|
36
|
+
name='MediaType',
|
|
47
37
|
fields=[
|
|
48
|
-
('
|
|
49
|
-
('
|
|
50
|
-
('
|
|
38
|
+
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
39
|
+
('type', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
40
|
+
('sub_type', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
41
|
+
('suffix', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, max_length=127)),
|
|
51
42
|
],
|
|
52
43
|
),
|
|
53
44
|
migrations.AddConstraint(
|
|
54
45
|
model_name='mediatype',
|
|
55
46
|
constraint=models.UniqueConstraint(fields=('type', 'sub_type', 'suffix'), name='oel_contents_uniq_t_st_sfx'),
|
|
56
47
|
),
|
|
57
|
-
migrations.
|
|
58
|
-
model_name='
|
|
59
|
-
|
|
48
|
+
migrations.AddField(
|
|
49
|
+
model_name='content',
|
|
50
|
+
name='learning_package',
|
|
51
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage'),
|
|
60
52
|
),
|
|
61
|
-
migrations.
|
|
62
|
-
model_name='
|
|
63
|
-
|
|
53
|
+
migrations.AddField(
|
|
54
|
+
model_name='content',
|
|
55
|
+
name='media_type',
|
|
56
|
+
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='oel_contents.mediatype'),
|
|
64
57
|
),
|
|
65
58
|
migrations.AddIndex(
|
|
66
|
-
model_name='
|
|
67
|
-
index=models.Index(fields=['learning_package', '-
|
|
59
|
+
model_name='content',
|
|
60
|
+
index=models.Index(fields=['learning_package', '-size'], name='oel_content_idx_lp_rsize'),
|
|
68
61
|
),
|
|
69
62
|
migrations.AddConstraint(
|
|
70
|
-
model_name='
|
|
63
|
+
model_name='content',
|
|
71
64
|
constraint=models.UniqueConstraint(fields=('learning_package', 'media_type', 'hash_digest'), name='oel_content_uniq_lc_media_type_hash_digest'),
|
|
72
65
|
),
|
|
73
66
|
]
|