-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathmodels.py
More file actions
271 lines (221 loc) · 10.6 KB
/
models.py
File metadata and controls
271 lines (221 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
"""
The model hierarchy is Component -> ComponentVersion -> Content.
A Component is an entity like a Problem or Video. It has enough information to
identify the Component and determine what the handler should be (e.g. XBlock
Problem), but little beyond that.
Components have one or more ComponentVersions, which represent saved versions of
that Component. Managing the publishing of these versions is handled through the
publishing app. Component maps 1:1 to PublishableEntity and ComponentVersion
maps 1:1 to PublishableEntityVersion.
Multiple pieces of Content may be associated with a ComponentVersion, through
the ComponentVersionMedia model. ComponentVersionMedia allows to specify a
ComponentVersion-local identifier. We're using this like a file path by
convention, but it's possible we might want to have special identifiers later.
"""
from __future__ import annotations
from typing import ClassVar
from django.db import models
from openedx_django_lib.fields import case_sensitive_char_field, key_field
from openedx_django_lib.managers import WithRelationsManager
from ..media.models import Media
from ..publishing.models import LearningPackage, PublishableEntityMixin, PublishableEntityVersionMixin
__all__ = [
"ComponentType",
"Component",
"ComponentVersion",
"ComponentVersionMedia",
]
class ComponentType(models.Model):
"""
Normalized representation of a type of Component.
The only namespace being used initially will be 'xblock.v1', but we will
probably add a few others over time, such as a component type to represent
packages of files for things like Files and Uploads or python_lib.zip files.
Make a ForeignKey against this table if you have to set policy based on the
type of Components–e.g. marking certain types of XBlocks as approved vs.
experimental for use in libraries.
"""
# We don't need the app default of 8-bytes for this primary key, but there
# is just a tiny chance that we'll use ComponentType in a novel, user-
# customizable way that will require more than 32K entries. So let's use a
# 4-byte primary key.
id = models.AutoField(primary_key=True)
# namespace and name work together to help figure out what Component needs
# to handle this data. A namespace is *required*. The namespace for XBlocks
# is "xblock.v1" (to match the setup.py entrypoint naming scheme).
namespace = case_sensitive_char_field(max_length=100, blank=False)
# name is a way to help sub-divide namespace if that's convenient. This
# field cannot be null, but it can be blank if it's not necessary. For an
# XBlock, this corresponds to tag, e.g. "video". It's also the block_type in
# the UsageKey.
name = case_sensitive_char_field(max_length=100, blank=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=[
"namespace",
"name",
],
name="oel_component_type_uniq_ns_n",
),
]
def __str__(self) -> str:
return f"{self.namespace}:{self.name}"
class Component(PublishableEntityMixin):
"""
This represents any Component that has ever existed in a LearningPackage.
What is a Component
-------------------
A Component is an entity like a Problem or Video. It has enough information
to identify itself and determine what the handler should be (e.g. XBlock
Problem), but little beyond that.
A Component will have many ComponentVersions over time, and most metadata is
associated with the ComponentVersion model and the Content that
ComponentVersions are associated with.
A Component belongs to exactly one LearningPackage.
A Component is 1:1 with PublishableEntity and has matching primary key
values. More specifically, ``Component.pk`` maps to
``Component.publishable_entity_id``, and any place where the Publishing API
module expects to get a ``PublishableEntity.id``, you can use a
``Component.pk`` instead.
Identifiers
-----------
Components have a ``publishable_entity`` OneToOneField to the ``publishing``
app's PublishableEntity field, and it uses this as its primary key. Please
see PublishableEntity's docstring for how you should use its ``uuid`` and
``key`` fields.
State Consistency
-----------------
The ``key`` field on Component's ``publishable_entity`` is dervied from the
``component_type`` and ``local_key`` fields in this model. We don't support
changing the keys yet, but if we do, those values need to be kept in sync.
How build on this model
-----------------------
Make a foreign key to the Component model when you need a stable reference
that will exist for as long as the LearningPackage itself exists.
"""
# Set up our custom manager. It has the same API as the default one, but selects related objects by default.
objects: ClassVar[WithRelationsManager[Component]] = WithRelationsManager( # type: ignore[assignment]
'component_type'
)
with_publishing_relations = WithRelationsManager(
'component_type',
'publishable_entity',
'publishable_entity__draft__version',
'publishable_entity__draft__version__componentversion',
'publishable_entity__published__version',
'publishable_entity__published__version__componentversion',
'publishable_entity__published__publish_log_record',
'publishable_entity__published__publish_log_record__publish_log',
)
# This foreign key is technically redundant because we're already locked to
# a single LearningPackage through our publishable_entity relation. However,
# having this foreign key directly allows us to make indexes that efficiently
# query by other Component fields within a given LearningPackage, which is
# going to be a common use case (and we can't make a compound index using
# columns from different tables).
learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
# What kind of Component are we? This will usually represent a specific
# XBlock block_type, but we want it to be more flexible in the long term.
component_type = models.ForeignKey(ComponentType, on_delete=models.PROTECT)
# local_key is an identifier that is local to the learning_package and
# component_type. The publishable.key should be calculated as a
# combination of component_type and local_key.
local_key = key_field()
class Meta:
constraints = [
# The combination of (component_type, local_key) is unique within
# a given LearningPackage. Note that this means it is possible to
# have two Components in the same LearningPackage to have the same
# local_key if the component_types are different. So for example,
# you could have a ProblemBlock and VideoBlock that both have the
# local_key "week_1".
models.UniqueConstraint(
fields=[
"learning_package",
"component_type",
"local_key",
],
name="oel_component_uniq_lc_ct_lk",
),
]
indexes = [
# Global Component-Type/Local-Key Index:
# * Search by the different Components fields across all Learning
# Packages on the site. This would be a support-oriented tool
# from Django Admin.
models.Index(
fields=[
"component_type",
"local_key",
],
name="oel_component_idx_ct_lk",
),
]
# These are for the Django Admin UI.
verbose_name = "Component"
verbose_name_plural = "Components"
def __str__(self) -> str:
return f"{self.component_type.namespace}:{self.component_type.name}:{self.local_key}"
class ComponentVersion(PublishableEntityVersionMixin):
"""
A particular version of a Component.
This holds the media using a M:M relationship with Content via
ComponentVersionMedia.
"""
# This is technically redundant, since we can get this through
# publishable_entity_version.publishable.component, but this is more
# convenient.
component = models.ForeignKey(
Component, on_delete=models.CASCADE, related_name="versions"
)
# The media relation holds the actual interesting data associated with this
# ComponentVersion.
media: models.ManyToManyField[Media, ComponentVersionMedia] = models.ManyToManyField(
Media,
through="ComponentVersionMedia",
related_name="component_versions",
)
class Meta:
verbose_name = "Component Version"
verbose_name_plural = "Component Versions"
class ComponentVersionMedia(models.Model):
"""
Determines the Content for a given ComponentVersion.
An ComponentVersion may be associated with multiple pieces of binary data.
For instance, a Video ComponentVersion might be associated with multiple
transcripts in different languages.
When Content is associated with an ComponentVersion, it has some local
key that is unique within the the context of that ComponentVersion. This
allows the ComponentVersion to do things like store an image file and
reference it by a "path" key.
Content is immutable and sharable across multiple ComponentVersions.
"""
component_version = models.ForeignKey(ComponentVersion, on_delete=models.CASCADE)
media = models.ForeignKey(Media, on_delete=models.RESTRICT)
# "key" is a reserved word for MySQL, so we're temporarily using the column
# name of "_key" to avoid breaking downstream tooling. A possible
# alternative name for this would be "path", since it's most often used as
# an internal file path. However, we might also want to put special
# identifiers that don't map as cleanly to file paths at some point.
key = key_field(db_column="_key")
class Meta:
constraints = [
# Uniqueness is only by ComponentVersion and key. If for some reason
# a ComponentVersion wants to associate the same piece of Media
# with two different identifiers, that is permitted.
models.UniqueConstraint(
fields=["component_version", "key"],
name="oel_cvcontent_uniq_cv_key",
),
]
indexes = [
models.Index(
fields=["media", "component_version"],
name="oel_cvmedia_c_cv",
),
models.Index(
fields=["component_version", "media"],
name="oel_cvmedia_cv_d",
),
]