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.
Files changed (24) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/contrib/media_server/views.py +2 -2
  3. openedx_learning/core/components/admin.py +22 -31
  4. openedx_learning/core/components/api.py +51 -47
  5. openedx_learning/core/components/migrations/0001_initial.py +12 -12
  6. openedx_learning/core/components/migrations/0002_alter_componentversioncontent_key.py +20 -0
  7. openedx_learning/core/components/models.py +37 -30
  8. openedx_learning/core/contents/admin.py +13 -20
  9. openedx_learning/core/contents/api.py +104 -94
  10. openedx_learning/core/contents/migrations/0001_initial.py +23 -30
  11. openedx_learning/core/contents/models.py +230 -149
  12. openedx_learning/core/publishing/migrations/0001_initial.py +2 -2
  13. openedx_learning/core/publishing/migrations/0002_alter_learningpackage_key_and_more.py +25 -0
  14. openedx_learning/core/publishing/models.py +41 -2
  15. openedx_learning/lib/fields.py +14 -2
  16. openedx_learning/lib/managers.py +6 -2
  17. {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/METADATA +4 -4
  18. {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/RECORD +24 -22
  19. openedx_tagging/core/tagging/data.py +1 -0
  20. openedx_tagging/core/tagging/models/base.py +36 -5
  21. openedx_tagging/core/tagging/rest_api/v1/serializers.py +1 -0
  22. {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/LICENSE.txt +0 -0
  23. {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/WHEEL +0 -0
  24. {openedx_learning-0.5.1.dist-info → openedx_learning-0.6.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
1
  """
2
2
  Open edX Learning ("Learning Core").
3
3
  """
4
- __version__ = "0.5.1"
4
+ __version__ = "0.6.1"
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
9
9
  from django.http import FileResponse, Http404
10
10
 
11
- from openedx_learning.core.components.api import get_component_version_content
11
+ from openedx_learning.core.components.api import look_up_component_version_content
12
12
 
13
13
 
14
14
  def component_asset(
@@ -28,7 +28,7 @@ def component_asset(
28
28
  * Serving from a different domain than the rest of the service
29
29
  """
30
30
  try:
31
- cvc = get_component_version_content(
31
+ cvc = look_up_component_version_content(
32
32
  learning_package_key, component_key, version_num, asset_path
33
33
  )
34
34
  except ObjectDoesNotExist:
@@ -9,7 +9,7 @@ from django.utils.safestring import SafeText
9
9
 
10
10
  from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin
11
11
 
12
- from .models import Component, ComponentVersion, ComponentVersionRawContent
12
+ from .models import Component, ComponentVersion, ComponentVersionContent
13
13
 
14
14
 
15
15
  class ComponentVersionInline(admin.TabularInline):
@@ -48,18 +48,18 @@ class ComponentAdmin(ReadOnlyModelAdmin):
48
48
  inlines = [ComponentVersionInline]
49
49
 
50
50
 
51
- class RawContentInline(admin.TabularInline):
51
+ class ContentInline(admin.TabularInline):
52
52
  """
53
- Django admin configuration for RawContent
53
+ Django admin configuration for Content
54
54
  """
55
- model = ComponentVersion.raw_contents.through
55
+ model = ComponentVersion.contents.through
56
56
 
57
57
  def get_queryset(self, request):
58
58
  queryset = super().get_queryset(request)
59
59
  return queryset.select_related(
60
- "raw_content",
61
- "raw_content__learning_package",
62
- "raw_content__text_content",
60
+ "content",
61
+ "content__learning_package",
62
+ "content__media_type",
63
63
  "component_version",
64
64
  "component_version__publishable_entity_version",
65
65
  "component_version__component",
@@ -73,7 +73,7 @@ class RawContentInline(admin.TabularInline):
73
73
  "rendered_data",
74
74
  ]
75
75
  readonly_fields = [
76
- "raw_content",
76
+ "content",
77
77
  "format_key",
78
78
  "format_size",
79
79
  "rendered_data",
@@ -85,7 +85,7 @@ class RawContentInline(admin.TabularInline):
85
85
 
86
86
  @admin.display(description="Size")
87
87
  def format_size(self, cvc_obj):
88
- return filesizeformat(cvc_obj.raw_content.size)
88
+ return filesizeformat(cvc_obj.content.size)
89
89
 
90
90
  @admin.display(description="Key")
91
91
  def format_key(self, cvc_obj):
@@ -108,7 +108,7 @@ class ComponentVersionAdmin(ReadOnlyModelAdmin):
108
108
  "title",
109
109
  "version_num",
110
110
  "created",
111
- "raw_contents",
111
+ "contents",
112
112
  ]
113
113
  fields = [
114
114
  "component",
@@ -118,7 +118,7 @@ class ComponentVersionAdmin(ReadOnlyModelAdmin):
118
118
  "created",
119
119
  ]
120
120
  list_display = ["component", "version_num", "uuid", "created"]
121
- inlines = [RawContentInline]
121
+ inlines = [ContentInline]
122
122
 
123
123
  def get_queryset(self, request):
124
124
  queryset = super().get_queryset(request)
@@ -129,12 +129,12 @@ class ComponentVersionAdmin(ReadOnlyModelAdmin):
129
129
  )
130
130
 
131
131
 
132
- def link_for_cvc(cvc_obj: ComponentVersionRawContent) -> str:
132
+ def link_for_cvc(cvc_obj: ComponentVersionContent) -> str:
133
133
  """
134
- Get the download URL for the given ComponentVersionRawContent instance
134
+ Get the download URL for the given ComponentVersionContent instance
135
135
  """
136
136
  return "/media_server/component_asset/{}/{}/{}/{}".format(
137
- cvc_obj.raw_content.learning_package.key,
137
+ cvc_obj.content.learning_package.key,
138
138
  cvc_obj.component_version.component.key,
139
139
  cvc_obj.component_version.version_num,
140
140
  cvc_obj.key,
@@ -151,27 +151,18 @@ def format_text_for_admin_display(text: str) -> SafeText:
151
151
  )
152
152
 
153
153
 
154
- def content_preview(cvc_obj: ComponentVersionRawContent) -> SafeText:
154
+ def content_preview(cvc_obj: ComponentVersionContent) -> SafeText:
155
155
  """
156
- Get the HTML to display a preview of the given ComponentVersionRawContent
156
+ Get the HTML to display a preview of the given ComponentVersionContent
157
157
  """
158
- raw_content_obj = cvc_obj.raw_content
158
+ content_obj = cvc_obj.content
159
159
 
160
- if raw_content_obj.media_type.type == "image":
160
+ if content_obj.media_type.type == "image":
161
161
  return format_html(
162
162
  '<img src="{}" style="max-width: 100%;" />',
163
- # TODO: configure with settings value:
164
- "/media_server/component_asset/{}/{}/{}/{}".format(
165
- cvc_obj.raw_content.learning_package.key,
166
- cvc_obj.component_version.component.key,
167
- cvc_obj.component_version.version_num,
168
- cvc_obj.key,
169
- ),
163
+ content_obj.file_url(),
170
164
  )
171
165
 
172
- if hasattr(raw_content_obj, "text_content"):
173
- return format_text_for_admin_display(
174
- raw_content_obj.text_content.text,
175
- )
176
-
177
- return format_html("This content type cannot be displayed.")
166
+ return format_text_for_admin_display(
167
+ content_obj.text or ""
168
+ )
@@ -18,28 +18,32 @@ from pathlib import Path
18
18
  from django.db.models import Q, QuerySet
19
19
  from django.db.transaction import atomic
20
20
 
21
- from ...lib.cache import lru_cache
22
21
  from ..publishing import api as publishing_api
23
- from .models import Component, ComponentType, ComponentVersion, ComponentVersionRawContent
22
+ from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent
24
23
 
25
24
 
26
- @lru_cache(maxsize=128)
27
- def get_or_create_component_type_id(namespace: str, name: str) -> int:
25
+ def get_or_create_component_type(namespace: str, name: str) -> ComponentType:
28
26
  """
29
27
  Get the ID of a ComponentType, and create if missing.
28
+
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.#
30
35
  """
31
36
  component_type, _created = ComponentType.objects.get_or_create(
32
37
  namespace=namespace,
33
38
  name=name,
34
39
  )
35
- return component_type.id
40
+ return component_type
36
41
 
37
42
 
38
43
  def create_component(
39
44
  learning_package_id: int,
40
45
  /,
41
- namespace: str,
42
- type_name: str,
46
+ component_type: ComponentType,
43
47
  local_key: str,
44
48
  created: datetime,
45
49
  created_by: int | None,
@@ -47,7 +51,7 @@ def create_component(
47
51
  """
48
52
  Create a new Component (an entity like a Problem or Video)
49
53
  """
50
- key = f"{namespace}:{type_name}@{local_key}"
54
+ key = f"{component_type.namespace}:{component_type.name}:{local_key}"
51
55
  with atomic():
52
56
  publishable_entity = publishing_api.create_publishable_entity(
53
57
  learning_package_id, key, created, created_by
@@ -55,7 +59,7 @@ def create_component(
55
59
  component = Component.objects.create(
56
60
  publishable_entity=publishable_entity,
57
61
  learning_package_id=learning_package_id,
58
- component_type_id=get_or_create_component_type_id(namespace, type_name),
62
+ component_type=component_type,
59
63
  local_key=local_key,
60
64
  )
61
65
  return component
@@ -101,10 +105,10 @@ def create_next_version(
101
105
  A very common pattern for making a new ComponentVersion is going to be "make
102
106
  it just like the last version, except changing these one or two things".
103
107
  Before calling this, you should create any new contents via the contents
104
- API, since ``content_to_replace`` needs RawContent IDs for the values.
108
+ API, since ``content_to_replace`` needs Content IDs for the values.
105
109
 
106
110
  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
111
+ local path/key for a file, to ``Content.id`` values. Using a `None` for
108
112
  a value in this dict means to delete that key in the next version.
109
113
 
110
114
  It is okay to mark entries for deletion that don't exist. For instance, if a
@@ -144,25 +148,25 @@ def create_next_version(
144
148
  component_id=component_pk,
145
149
  )
146
150
  # 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
151
+ for key, content_pk in content_to_replace.items():
152
+ # If the content_pk is None, it means we want to remove the
149
153
  # 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
+ # we add our key->content_pk mapping to the next version.
155
+ if content_pk is not None:
156
+ ComponentVersionContent.objects.create(
157
+ content_id=content_pk,
154
158
  component_version=component_version,
155
159
  key=key,
156
160
  learner_downloadable=False,
157
161
  )
158
162
  # Now copy any old associations that existed, as long as they aren't
159
163
  # in conflict with the new stuff or marked for deletion.
160
- last_version_content_mapping = ComponentVersionRawContent.objects \
161
- .filter(component_version=last_version)
164
+ last_version_content_mapping = ComponentVersionContent.objects \
165
+ .filter(component_version=last_version)
162
166
  for cvrc in last_version_content_mapping:
163
167
  if cvrc.key not in content_to_replace:
164
- ComponentVersionRawContent.objects.create(
165
- raw_content_id=cvrc.raw_content_id,
168
+ ComponentVersionContent.objects.create(
169
+ content_id=cvrc.content_id,
166
170
  component_version=component_version,
167
171
  key=cvrc.key,
168
172
  learner_downloadable=cvrc.learner_downloadable,
@@ -174,8 +178,7 @@ def create_next_version(
174
178
  def create_component_and_version(
175
179
  learning_package_id: int,
176
180
  /,
177
- namespace: str,
178
- type_name: str,
181
+ component_type: ComponentType,
179
182
  local_key: str,
180
183
  title: str,
181
184
  created: datetime,
@@ -186,7 +189,7 @@ def create_component_and_version(
186
189
  """
187
190
  with atomic():
188
191
  component = create_component(
189
- learning_package_id, namespace, type_name, local_key, created, created_by
192
+ learning_package_id, component_type, local_key, created, created_by
190
193
  )
191
194
  component_version = create_component_version(
192
195
  component.pk,
@@ -282,27 +285,29 @@ def get_components(
282
285
  qset = qset.filter(component_type__name__in=type_names)
283
286
  if draft_title is not None:
284
287
  qset = qset.filter(
285
- publishable_entity__draft__version__title__icontains=draft_title
288
+ Q(publishable_entity__draft__version__title__icontains=draft_title) |
289
+ Q(local_key__icontains=draft_title)
286
290
  )
287
291
  if published_title is not None:
288
292
  qset = qset.filter(
289
- publishable_entity__published__version__title__icontains=published_title
293
+ Q(publishable_entity__published__version__title__icontains=published_title) |
294
+ Q(local_key__icontains=published_title)
290
295
  )
291
296
 
292
297
  return qset
293
298
 
294
299
 
295
- def get_component_version_content(
300
+ def look_up_component_version_content(
296
301
  learning_package_key: str,
297
302
  component_key: str,
298
303
  version_num: int,
299
304
  key: Path,
300
- ) -> ComponentVersionRawContent:
305
+ ) -> ComponentVersionContent:
301
306
  """
302
- Look up ComponentVersionRawContent by human readable keys.
307
+ Look up ComponentVersionContent by human readable keys.
303
308
 
304
309
  Can raise a django.core.exceptions.ObjectDoesNotExist error if there is no
305
- matching ComponentVersionRawContent.
310
+ matching ComponentVersionContent.
306
311
  """
307
312
  queries = (
308
313
  Q(component_version__component__learning_package__key=learning_package_key)
@@ -310,30 +315,29 @@ def get_component_version_content(
310
315
  & Q(component_version__publishable_entity_version__version_num=version_num)
311
316
  & Q(key=key)
312
317
  )
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(
318
+ return ComponentVersionContent.objects \
319
+ .select_related(
320
+ "content",
321
+ "content__media_type",
322
+ "component_version",
323
+ "component_version__component",
324
+ "component_version__component__learning_package",
325
+ ).get(queries)
326
+
327
+
328
+ def create_component_version_content(
325
329
  component_version_id: int,
330
+ content_id: int,
326
331
  /,
327
- raw_content_id: int,
328
332
  key: str,
329
333
  learner_downloadable=False,
330
- ) -> ComponentVersionRawContent:
334
+ ) -> ComponentVersionContent:
331
335
  """
332
- Add a RawContent to the given ComponentVersion
336
+ Add a Content to the given ComponentVersion
333
337
  """
334
- cvrc, _created = ComponentVersionRawContent.objects.get_or_create(
338
+ cvrc, _created = ComponentVersionContent.objects.get_or_create(
335
339
  component_version_id=component_version_id,
336
- raw_content_id=raw_content_id,
340
+ content_id=content_id,
337
341
  key=key,
338
342
  learner_downloadable=learner_downloadable,
339
343
  )
@@ -1,4 +1,4 @@
1
- # Generated by Django 3.2.23 on 2024-01-31 05:34
1
+ # Generated by Django 3.2.23 on 2024-02-06 03:23
2
2
 
3
3
  import uuid
4
4
 
@@ -13,8 +13,8 @@ class Migration(migrations.Migration):
13
13
  initial = True
14
14
 
15
15
  dependencies = [
16
- ('oel_publishing', '0001_initial'),
17
16
  ('oel_contents', '0001_initial'),
17
+ ('oel_publishing', '0001_initial'),
18
18
  ]
19
19
 
20
20
  operations = [
@@ -49,20 +49,20 @@ class Migration(migrations.Migration):
49
49
  },
50
50
  ),
51
51
  migrations.CreateModel(
52
- name='ComponentVersionRawContent',
52
+ name='ComponentVersionContent',
53
53
  fields=[
54
54
  ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
55
55
  ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
56
56
  ('key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500)),
57
57
  ('learner_downloadable', models.BooleanField(default=False)),
58
58
  ('component_version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_components.componentversion')),
59
- ('raw_content', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='oel_contents.rawcontent')),
59
+ ('content', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='oel_contents.content')),
60
60
  ],
61
61
  ),
62
62
  migrations.AddField(
63
63
  model_name='componentversion',
64
- name='raw_contents',
65
- field=models.ManyToManyField(related_name='component_versions', through='oel_components.ComponentVersionRawContent', to='oel_contents.RawContent'),
64
+ name='contents',
65
+ field=models.ManyToManyField(related_name='component_versions', through='oel_components.ComponentVersionContent', to='oel_contents.Content'),
66
66
  ),
67
67
  migrations.AddField(
68
68
  model_name='component',
@@ -75,16 +75,16 @@ class Migration(migrations.Migration):
75
75
  field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage'),
76
76
  ),
77
77
  migrations.AddIndex(
78
- model_name='componentversionrawcontent',
79
- index=models.Index(fields=['raw_content', 'component_version'], name='oel_cvrawcontent_c_cv'),
78
+ model_name='componentversioncontent',
79
+ index=models.Index(fields=['content', 'component_version'], name='oel_cvcontent_c_cv'),
80
80
  ),
81
81
  migrations.AddIndex(
82
- model_name='componentversionrawcontent',
83
- index=models.Index(fields=['component_version', 'raw_content'], name='oel_cvrawcontent_cv_d'),
82
+ model_name='componentversioncontent',
83
+ index=models.Index(fields=['component_version', 'content'], name='oel_cvcontent_cv_d'),
84
84
  ),
85
85
  migrations.AddConstraint(
86
- model_name='componentversionrawcontent',
87
- constraint=models.UniqueConstraint(fields=('component_version', 'key'), name='oel_cvrawcontent_uniq_cv_key'),
86
+ model_name='componentversioncontent',
87
+ constraint=models.UniqueConstraint(fields=('component_version', 'key'), name='oel_cvcontent_uniq_cv_key'),
88
88
  ),
89
89
  migrations.AddIndex(
90
90
  model_name='component',
@@ -0,0 +1,20 @@
1
+ # Generated by Django 4.2.10 on 2024-02-14 22:02
2
+
3
+ from django.db import migrations
4
+
5
+ import openedx_learning.lib.fields
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('oel_components', '0001_initial'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AlterField(
16
+ model_name='componentversioncontent',
17
+ name='key',
18
+ field=openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, db_column='_key', max_length=500),
19
+ ),
20
+ ]
@@ -1,5 +1,5 @@
1
1
  """
2
- The model hierarchy is Component -> ComponentVersion -> RawContent.
2
+ The model hierarchy is Component -> ComponentVersion -> Content.
3
3
 
4
4
  A Component is an entity like a Problem or Video. It has enough information to
5
5
  identify the Component and determine what the handler should be (e.g. XBlock
@@ -10,20 +10,18 @@ that Component. Managing the publishing of these versions is handled through the
10
10
  publishing app. Component maps 1:1 to PublishableEntity and ComponentVersion
11
11
  maps 1:1 to PublishableEntityVersion.
12
12
 
13
- Multiple pieces of RawContent may be associated with a ComponentVersion, through
14
- the ComponentVersionRawContent model. ComponentVersionRawContent allows to
15
- specify a ComponentVersion-local identifier. We're using this like a file path
16
- by convention, but it's possible we might want to have special identifiers
17
- later.
13
+ Multiple pieces of Content may be associated with a ComponentVersion, through
14
+ the ComponentVersionContent model. ComponentVersionContent allows to specify a
15
+ ComponentVersion-local identifier. We're using this like a file path by
16
+ convention, but it's possible we might want to have special identifiers later.
18
17
  """
19
18
  from __future__ import annotations
20
19
 
21
20
  from django.db import models
22
21
 
23
- from openedx_learning.lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field
24
- from openedx_learning.lib.managers import WithRelationsManager
25
-
26
- from ..contents.models import RawContent
22
+ from ...lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field
23
+ from ...lib.managers import WithRelationsManager
24
+ from ..contents.models import Content
27
25
  from ..publishing.model_mixins import PublishableEntityMixin, PublishableEntityVersionMixin
28
26
  from ..publishing.models import LearningPackage
29
27
 
@@ -40,6 +38,10 @@ class ComponentType(models.Model):
40
38
  type of Components–e.g. marking certain types of XBlocks as approved vs.
41
39
  experimental for use in libraries.
42
40
  """
41
+ # We don't need the app default of 8-bytes for this primary key, but there
42
+ # is just a tiny chance that we'll use ComponentType in a novel, user-
43
+ # customizable way that will require more than 32K entries. So let's use a
44
+ # 4-byte primary key.
43
45
  id = models.AutoField(primary_key=True)
44
46
 
45
47
  # namespace and name work together to help figure out what Component needs
@@ -79,7 +81,7 @@ class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
79
81
  Problem), but little beyond that.
80
82
 
81
83
  A Component will have many ComponentVersions over time, and most metadata is
82
- associated with the ComponentVersion model and the RawContent that
84
+ associated with the ComponentVersion model and the Content that
83
85
  ComponentVersions are associated with.
84
86
 
85
87
  A Component belongs to exactly one LearningPackage.
@@ -187,8 +189,8 @@ class ComponentVersion(PublishableEntityVersionMixin):
187
189
  """
188
190
  A particular version of a Component.
189
191
 
190
- This holds the content using a M:M relationship with RawContent via
191
- ComponentVersionRawContent.
192
+ This holds the content using a M:M relationship with Content via
193
+ ComponentVersionContent.
192
194
  """
193
195
  # Tell mypy what type our objects manager has.
194
196
  # It's actually PublishableEntityVersionMixinManager, but that has the exact
@@ -202,11 +204,11 @@ class ComponentVersion(PublishableEntityVersionMixin):
202
204
  Component, on_delete=models.CASCADE, related_name="versions"
203
205
  )
204
206
 
205
- # The raw_contents hold the actual interesting data associated with this
207
+ # The contents hold the actual interesting data associated with this
206
208
  # ComponentVersion.
207
- raw_contents: models.ManyToManyField[RawContent, ComponentVersionRawContent] = models.ManyToManyField(
208
- RawContent,
209
- through="ComponentVersionRawContent",
209
+ contents: models.ManyToManyField[Content, ComponentVersionContent] = models.ManyToManyField(
210
+ Content,
211
+ through="ComponentVersionContent",
210
212
  related_name="component_versions",
211
213
  )
212
214
 
@@ -215,32 +217,37 @@ class ComponentVersion(PublishableEntityVersionMixin):
215
217
  verbose_name_plural = "Component Versions"
216
218
 
217
219
 
218
- class ComponentVersionRawContent(models.Model):
220
+ class ComponentVersionContent(models.Model):
219
221
  """
220
- Determines the RawContent for a given ComponentVersion.
222
+ Determines the Content for a given ComponentVersion.
221
223
 
222
224
  An ComponentVersion may be associated with multiple pieces of binary data.
223
225
  For instance, a Video ComponentVersion might be associated with multiple
224
226
  transcripts in different languages.
225
227
 
226
- When RawContent is associated with an ComponentVersion, it has some local
228
+ When Content is associated with an ComponentVersion, it has some local
227
229
  key that is unique within the the context of that ComponentVersion. This
228
230
  allows the ComponentVersion to do things like store an image file and
229
231
  reference it by a "path" key.
230
232
 
231
- RawContent is immutable and sharable across multiple ComponentVersions and
232
- even across LearningPackages.
233
+ Content is immutable and sharable across multiple ComponentVersions.
233
234
  """
234
235
 
235
- raw_content = models.ForeignKey(RawContent, on_delete=models.RESTRICT)
236
236
  component_version = models.ForeignKey(ComponentVersion, on_delete=models.CASCADE)
237
+ content = models.ForeignKey(Content, on_delete=models.RESTRICT)
237
238
 
238
239
  uuid = immutable_uuid_field()
239
- key = key_field()
240
+
241
+ # "key" is a reserved word for MySQL, so we're temporarily using the column
242
+ # name of "_key" to avoid breaking downstream tooling. A possible
243
+ # alternative name for this would be "path", since it's most often used as
244
+ # an internal file path. However, we might also want to put special
245
+ # identifiers that don't map as cleanly to file paths at some point.
246
+ key = key_field(db_column="_key")
240
247
 
241
248
  # Long explanation for the ``learner_downloadable`` field:
242
249
  #
243
- # Is this RawContent downloadable during the learning experience? This is
250
+ # Is this Content downloadable during the learning experience? This is
244
251
  # NOT about public vs. private permissions on course assets, as that will be
245
252
  # a policy that can be changed independently of new versions of the content.
246
253
  # For instance, a course team could decide to flip their course assets from
@@ -282,16 +289,16 @@ class ComponentVersionRawContent(models.Model):
282
289
  # with two different identifiers, that is permitted.
283
290
  models.UniqueConstraint(
284
291
  fields=["component_version", "key"],
285
- name="oel_cvrawcontent_uniq_cv_key",
292
+ name="oel_cvcontent_uniq_cv_key",
286
293
  ),
287
294
  ]
288
295
  indexes = [
289
296
  models.Index(
290
- fields=["raw_content", "component_version"],
291
- name="oel_cvrawcontent_c_cv",
297
+ fields=["content", "component_version"],
298
+ name="oel_cvcontent_c_cv",
292
299
  ),
293
300
  models.Index(
294
- fields=["component_version", "raw_content"],
295
- name="oel_cvrawcontent_cv_d",
301
+ fields=["component_version", "content"],
302
+ name="oel_cvcontent_cv_d",
296
303
  ),
297
304
  ]
@@ -6,13 +6,13 @@ from django.utils.html import format_html
6
6
 
7
7
  from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin
8
8
 
9
- from .models import RawContent
9
+ from .models import Content
10
10
 
11
11
 
12
- @admin.register(RawContent)
13
- class RawContentAdmin(ReadOnlyModelAdmin):
12
+ @admin.register(Content)
13
+ class ContentAdmin(ReadOnlyModelAdmin):
14
14
  """
15
- Django admin for RawContent model
15
+ Django admin for Content model
16
16
  """
17
17
  list_display = [
18
18
  "hash_digest",
@@ -21,6 +21,7 @@ class RawContentAdmin(ReadOnlyModelAdmin):
21
21
  "media_type",
22
22
  "size",
23
23
  "created",
24
+ "has_file",
24
25
  ]
25
26
  fields = [
26
27
  "learning_package",
@@ -30,30 +31,22 @@ class RawContentAdmin(ReadOnlyModelAdmin):
30
31
  "created",
31
32
  "file_link",
32
33
  "text_preview",
33
- ]
34
- readonly_fields = [
35
- "learning_package",
36
- "hash_digest",
37
- "media_type",
38
- "size",
39
- "created",
40
- "file_link",
41
- "text_preview",
34
+ "has_file",
42
35
  ]
43
36
  list_filter = ("media_type", "learning_package")
44
37
  search_fields = ("hash_digest",)
45
38
 
46
- def file_link(self, raw_content):
39
+ def file_link(self, content: Content):
40
+ if not content.has_file:
41
+ return ""
42
+
47
43
  return format_html(
48
44
  '<a href="{}">Download</a>',
49
- raw_content.file.url,
45
+ content.file_url(),
50
46
  )
51
47
 
52
- def text_preview(self, raw_content):
53
- if not hasattr(raw_content, "text_content"):
54
- return "(not available)"
55
-
48
+ def text_preview(self, content: Content):
56
49
  return format_html(
57
50
  '<pre style="white-space: pre-wrap;">\n{}\n</pre>',
58
- raw_content.text_content.text,
51
+ content.text,
59
52
  )