openedx-learning 0.18.3__py2.py3-none-any.whl → 0.19.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/api/authoring.py +1 -0
- openedx_learning/api/authoring_models.py +1 -1
- openedx_learning/apps/authoring/components/api.py +18 -5
- openedx_learning/apps/authoring/components/apps.py +1 -1
- openedx_learning/apps/authoring/components/models.py +9 -14
- openedx_learning/apps/authoring/contents/models.py +1 -1
- openedx_learning/apps/authoring/publishing/admin.py +3 -0
- openedx_learning/apps/authoring/publishing/api.py +508 -3
- openedx_learning/apps/authoring/publishing/apps.py +9 -0
- openedx_learning/apps/authoring/publishing/migrations/0003_containers.py +54 -0
- openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py +21 -0
- openedx_learning/apps/authoring/publishing/models/__init__.py +27 -0
- openedx_learning/apps/authoring/publishing/models/container.py +70 -0
- openedx_learning/apps/authoring/publishing/models/draft_published.py +95 -0
- openedx_learning/apps/authoring/publishing/models/entity_list.py +69 -0
- openedx_learning/apps/authoring/publishing/models/learning_package.py +75 -0
- openedx_learning/apps/authoring/publishing/models/publish_log.py +106 -0
- openedx_learning/apps/authoring/publishing/{model_mixins.py → models/publishable_entity.py} +289 -41
- openedx_learning/apps/authoring/units/__init__.py +0 -0
- openedx_learning/apps/authoring/units/api.py +305 -0
- openedx_learning/apps/authoring/units/apps.py +25 -0
- openedx_learning/apps/authoring/units/migrations/0001_initial.py +36 -0
- openedx_learning/apps/authoring/units/migrations/__init__.py +0 -0
- openedx_learning/apps/authoring/units/models.py +50 -0
- openedx_learning/contrib/media_server/apps.py +1 -1
- openedx_learning/lib/managers.py +7 -1
- openedx_learning/lib/validators.py +1 -1
- {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/METADATA +3 -3
- {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/RECORD +37 -24
- {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/WHEEL +1 -1
- openedx_tagging/core/tagging/api.py +4 -4
- openedx_tagging/core/tagging/models/base.py +1 -1
- openedx_tagging/core/tagging/rest_api/v1/permissions.py +1 -1
- openedx_tagging/core/tagging/rules.py +1 -2
- openedx_learning/apps/authoring/publishing/models.py +0 -517
- {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/LICENSE.txt +0 -0
- {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,265 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
PublishableEntity model and PublishableEntityVersion + mixins
|
|
3
3
|
"""
|
|
4
|
-
from
|
|
5
|
-
|
|
4
|
+
from datetime import datetime
|
|
6
5
|
from functools import cached_property
|
|
6
|
+
from typing import ClassVar, Self
|
|
7
7
|
|
|
8
|
+
from django.conf import settings
|
|
8
9
|
from django.core.exceptions import ImproperlyConfigured
|
|
10
|
+
from django.core.validators import MinValueValidator
|
|
9
11
|
from django.db import models
|
|
10
|
-
from django.
|
|
12
|
+
from django.utils.translation import gettext as _
|
|
13
|
+
|
|
14
|
+
from openedx_learning.lib.fields import (
|
|
15
|
+
case_insensitive_char_field,
|
|
16
|
+
immutable_uuid_field,
|
|
17
|
+
key_field,
|
|
18
|
+
manual_date_time_field,
|
|
19
|
+
)
|
|
20
|
+
from openedx_learning.lib.managers import WithRelationsManager
|
|
21
|
+
|
|
22
|
+
from .learning_package import LearningPackage
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PublishableEntity(models.Model):
|
|
26
|
+
"""
|
|
27
|
+
This represents any publishable thing that has ever existed in a
|
|
28
|
+
LearningPackage. It serves as a stable model that will not go away even if
|
|
29
|
+
these things are later unpublished or deleted.
|
|
30
|
+
|
|
31
|
+
A PublishableEntity belongs to exactly one LearningPackage.
|
|
32
|
+
|
|
33
|
+
Examples of Publishable Entities
|
|
34
|
+
--------------------------------
|
|
35
|
+
|
|
36
|
+
Components (e.g. VideoBlock, ProblemBlock), Units, and Sections/Subsections
|
|
37
|
+
would all be considered Publishable Entites. But anything that can be
|
|
38
|
+
imported, exported, published, and reverted in a course or library could be
|
|
39
|
+
modeled as a PublishableEntity, including things like Grading Policy or
|
|
40
|
+
possibly Taxonomies (?).
|
|
41
|
+
|
|
42
|
+
How to use this model
|
|
43
|
+
---------------------
|
|
44
|
+
|
|
45
|
+
The publishing app understands that publishable entities exist, along with
|
|
46
|
+
their drafts and published versions. It has some basic metadata, such as
|
|
47
|
+
identifiers, who created it, and when it was created. It's meant to
|
|
48
|
+
encapsulate the draft and publishing related aspects of your content, but
|
|
49
|
+
the ``publishing`` app doesn't know anything about the actual content being
|
|
50
|
+
referenced.
|
|
51
|
+
|
|
52
|
+
You have to provide actual meaning to PublishableEntity by creating your own
|
|
53
|
+
models that will represent your particular content and associating them to
|
|
54
|
+
PublishableEntity via a OneToOneField with primary_key=True. The easiest way
|
|
55
|
+
to do this is to have your model inherit from PublishableEntityMixin.
|
|
56
|
+
|
|
57
|
+
Identifiers
|
|
58
|
+
-----------
|
|
59
|
+
The UUID is globally unique and should be treated as immutable.
|
|
60
|
+
|
|
61
|
+
The key field *is* mutable, but changing it will affect all
|
|
62
|
+
PublishedEntityVersions. They are locally unique within the LearningPackage.
|
|
63
|
+
|
|
64
|
+
If you are referencing this model from within the same process, use a
|
|
65
|
+
foreign key to the id. If you are referencing this PublishedEntity from an
|
|
66
|
+
external system/service, use the UUID. The key is the part that is most
|
|
67
|
+
likely to be human-readable, and may be exported/copied, but try not to rely
|
|
68
|
+
on it, since this value may change.
|
|
69
|
+
|
|
70
|
+
Note: When we actually implement the ability to change identifiers, we
|
|
71
|
+
should make a history table and a modified attribute on this model.
|
|
72
|
+
|
|
73
|
+
Why are Identifiers in this Model?
|
|
74
|
+
----------------------------------
|
|
75
|
+
|
|
76
|
+
A PublishableEntity never stands alone–it's always intended to be used with
|
|
77
|
+
a 1:1 model like Component or Unit. So why have all the identifiers in this
|
|
78
|
+
model instead of storing them in those other models? Two reasons:
|
|
79
|
+
|
|
80
|
+
* Published things need to have the right identifiers so they can be used
|
|
81
|
+
throughout the system, and the UUID is serving the role of ISBN in physical
|
|
82
|
+
book publishing.
|
|
83
|
+
* We want to be able to enforce the idea that "key" is locally unique across
|
|
84
|
+
all PublishableEntities within a given LearningPackage. Component and Unit
|
|
85
|
+
can't do that without a shared model.
|
|
86
|
+
|
|
87
|
+
That being said, models that build on PublishableEntity are free to add
|
|
88
|
+
their own identifiers if it's useful to do so.
|
|
89
|
+
|
|
90
|
+
Why not Inherit from this Model?
|
|
91
|
+
--------------------------------
|
|
92
|
+
|
|
93
|
+
Django supports multi-table inheritance:
|
|
94
|
+
|
|
95
|
+
https://docs.djangoproject.com/en/4.2/topics/db/models/#multi-table-inheritance
|
|
96
|
+
|
|
97
|
+
We don't use that, primarily because we want to more clearly decouple
|
|
98
|
+
publishing concerns from the rest of the logic around Components, Units,
|
|
99
|
+
etc. If you made a Component and ComponentVersion models that subclassed
|
|
100
|
+
PublishableEntity and PublishableEntityVersion, and then accessed
|
|
101
|
+
``component.versions``, you might expect ComponentVersions to come back and
|
|
102
|
+
be surprised when you get EntityVersions instead.
|
|
103
|
+
|
|
104
|
+
In general, we want freedom to add new Publishing models, fields, and
|
|
105
|
+
methods without having to worry about the downstream name collisions with
|
|
106
|
+
other apps (many of which might live in other repositories). The helper
|
|
107
|
+
mixins will provide a little syntactic sugar to make common access patterns
|
|
108
|
+
more convenient, like file access.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
uuid = immutable_uuid_field()
|
|
112
|
+
learning_package = models.ForeignKey(
|
|
113
|
+
LearningPackage,
|
|
114
|
+
on_delete=models.CASCADE,
|
|
115
|
+
related_name="publishable_entities",
|
|
116
|
+
)
|
|
11
117
|
|
|
12
|
-
|
|
118
|
+
# "key" is a reserved word for MySQL, so we're temporarily using the column
|
|
119
|
+
# name of "_key" to avoid breaking downstream tooling. Consider renaming
|
|
120
|
+
# this later.
|
|
121
|
+
key = key_field(db_column="_key")
|
|
122
|
+
|
|
123
|
+
created = manual_date_time_field()
|
|
124
|
+
created_by = models.ForeignKey(
|
|
125
|
+
settings.AUTH_USER_MODEL,
|
|
126
|
+
on_delete=models.SET_NULL,
|
|
127
|
+
null=True,
|
|
128
|
+
blank=True,
|
|
129
|
+
)
|
|
130
|
+
can_stand_alone = models.BooleanField(
|
|
131
|
+
default=True,
|
|
132
|
+
help_text=_("Set to True when created independently, False when created as part of a container."),
|
|
133
|
+
)
|
|
13
134
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
135
|
+
class Meta:
|
|
136
|
+
constraints = [
|
|
137
|
+
# Keys are unique within a given LearningPackage.
|
|
138
|
+
models.UniqueConstraint(
|
|
139
|
+
fields=[
|
|
140
|
+
"learning_package",
|
|
141
|
+
"key",
|
|
142
|
+
],
|
|
143
|
+
name="oel_pub_ent_uniq_lp_key",
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
indexes = [
|
|
147
|
+
# Global Key Index:
|
|
148
|
+
# * Search by key across all PublishableEntities on the site. This
|
|
149
|
+
# would be a support-oriented tool from Django Admin.
|
|
150
|
+
models.Index(
|
|
151
|
+
fields=["key"],
|
|
152
|
+
name="oel_pub_ent_idx_key",
|
|
153
|
+
),
|
|
154
|
+
# LearningPackage (reverse) Created Index:
|
|
155
|
+
# * Search for most recently *created* PublishableEntities for a
|
|
156
|
+
# given LearningPackage, since they're the most likely to be
|
|
157
|
+
# actively worked on.
|
|
158
|
+
models.Index(
|
|
159
|
+
fields=["learning_package", "-created"],
|
|
160
|
+
name="oel_pub_ent_idx_lp_rcreated",
|
|
161
|
+
),
|
|
162
|
+
]
|
|
163
|
+
# These are for the Django Admin UI.
|
|
164
|
+
verbose_name = "Publishable Entity"
|
|
165
|
+
verbose_name_plural = "Publishable Entities"
|
|
166
|
+
|
|
167
|
+
def __str__(self):
|
|
168
|
+
return f"{self.key}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class PublishableEntityVersion(models.Model):
|
|
172
|
+
"""
|
|
173
|
+
A particular version of a PublishableEntity.
|
|
174
|
+
|
|
175
|
+
This model has its own ``uuid`` so that it can be referenced directly. The
|
|
176
|
+
``uuid`` should be treated as immutable.
|
|
177
|
+
|
|
178
|
+
PublishableEntityVersions are created once and never updated. So for
|
|
179
|
+
instance, the ``title`` should never be modified.
|
|
180
|
+
|
|
181
|
+
Like PublishableEntity, the data in this model is only enough to cover the
|
|
182
|
+
parts that are most important for the actual process of managing drafts and
|
|
183
|
+
publishes. You will want to create your own models to represent the actual
|
|
184
|
+
content data that's associated with this PublishableEntityVersion, and
|
|
185
|
+
connect them using a OneToOneField with primary_key=True. The easiest way to
|
|
186
|
+
do this is to inherit from PublishableEntityVersionMixin. Be sure to treat
|
|
187
|
+
these versioned models in your app as immutable as well.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
uuid = immutable_uuid_field()
|
|
191
|
+
entity = models.ForeignKey(
|
|
192
|
+
PublishableEntity, on_delete=models.CASCADE, related_name="versions"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Most publishable things will have some sort of title, but blanks are
|
|
196
|
+
# allowed for those that don't require one.
|
|
197
|
+
title = case_insensitive_char_field(max_length=500, blank=True, default="")
|
|
198
|
+
|
|
199
|
+
# The version_num starts at 1 and increments by 1 with each new version for
|
|
200
|
+
# a given PublishableEntity. Doing it this way makes it more convenient for
|
|
201
|
+
# users to refer to than a hash or UUID value. It also helps us catch race
|
|
202
|
+
# conditions on save, by setting a unique constraint on the entity and
|
|
203
|
+
# version_num.
|
|
204
|
+
version_num = models.PositiveIntegerField(
|
|
205
|
+
null=False,
|
|
206
|
+
validators=[MinValueValidator(1)],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# All PublishableEntityVersions created as part of the same publish should
|
|
210
|
+
# have the exact same created datetime (not off by a handful of
|
|
211
|
+
# microseconds).
|
|
212
|
+
created = manual_date_time_field()
|
|
213
|
+
|
|
214
|
+
# User who created the PublishableEntityVersion. This can be null if the
|
|
215
|
+
# user is later removed. Open edX in general doesn't let you remove users,
|
|
216
|
+
# but we should try to model it so that this is possible eventually.
|
|
217
|
+
created_by = models.ForeignKey(
|
|
218
|
+
settings.AUTH_USER_MODEL,
|
|
219
|
+
on_delete=models.SET_NULL,
|
|
220
|
+
null=True,
|
|
221
|
+
blank=True,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
class Meta:
|
|
225
|
+
constraints = [
|
|
226
|
+
# Prevent the situation where we have multiple
|
|
227
|
+
# PublishableEntityVersions claiming to be the same version_num for
|
|
228
|
+
# a given PublishableEntity. This can happen if there's a race
|
|
229
|
+
# condition between concurrent editors in different browsers,
|
|
230
|
+
# working on the same Publishable. With this constraint, one of
|
|
231
|
+
# those processes will raise an IntegrityError.
|
|
232
|
+
models.UniqueConstraint(
|
|
233
|
+
fields=[
|
|
234
|
+
"entity",
|
|
235
|
+
"version_num",
|
|
236
|
+
],
|
|
237
|
+
name="oel_pv_uniq_entity_version_num",
|
|
238
|
+
)
|
|
239
|
+
]
|
|
240
|
+
indexes = [
|
|
241
|
+
# LearningPackage (reverse) Created Index:
|
|
242
|
+
# * Make it cheap to find the most recently created
|
|
243
|
+
# PublishableEntityVersions for a given LearningPackage. This
|
|
244
|
+
# represents the most recently saved work for a LearningPackage
|
|
245
|
+
# and would be the most likely areas to get worked on next.
|
|
246
|
+
models.Index(
|
|
247
|
+
fields=["entity", "-created"],
|
|
248
|
+
name="oel_pv_idx_entity_rcreated",
|
|
249
|
+
),
|
|
250
|
+
# Title Index:
|
|
251
|
+
# * Search by title.
|
|
252
|
+
models.Index(
|
|
253
|
+
fields=[
|
|
254
|
+
"title",
|
|
255
|
+
],
|
|
256
|
+
name="oel_pv_idx_title",
|
|
257
|
+
),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
# These are for the Django Admin UI.
|
|
261
|
+
verbose_name = "Publishable Entity Version"
|
|
262
|
+
verbose_name_plural = "Publishable Entity Versions"
|
|
19
263
|
|
|
20
264
|
|
|
21
265
|
class PublishableEntityMixin(models.Model):
|
|
@@ -28,17 +272,12 @@ class PublishableEntityMixin(models.Model):
|
|
|
28
272
|
the publishing app's api.register_content_models (see its docstring for
|
|
29
273
|
details).
|
|
30
274
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"publishable_entity__published",
|
|
38
|
-
"publishable_entity__draft",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
objects: models.Manager[PublishableEntityMixin] = PublishableEntityMixinManager()
|
|
275
|
+
# select these related entities by default for all queries
|
|
276
|
+
objects: ClassVar[WithRelationsManager[Self]] = WithRelationsManager(
|
|
277
|
+
"publishable_entity",
|
|
278
|
+
"publishable_entity__published",
|
|
279
|
+
"publishable_entity__draft",
|
|
280
|
+
)
|
|
42
281
|
|
|
43
282
|
publishable_entity = models.OneToOneField(
|
|
44
283
|
PublishableEntity, on_delete=models.CASCADE, primary_key=True
|
|
@@ -49,15 +288,15 @@ class PublishableEntityMixin(models.Model):
|
|
|
49
288
|
return self.VersioningHelper(self)
|
|
50
289
|
|
|
51
290
|
@property
|
|
52
|
-
def uuid(self):
|
|
291
|
+
def uuid(self) -> str:
|
|
53
292
|
return self.publishable_entity.uuid
|
|
54
293
|
|
|
55
294
|
@property
|
|
56
|
-
def key(self):
|
|
295
|
+
def key(self) -> str:
|
|
57
296
|
return self.publishable_entity.key
|
|
58
297
|
|
|
59
298
|
@property
|
|
60
|
-
def created(self):
|
|
299
|
+
def created(self) -> datetime:
|
|
61
300
|
return self.publishable_entity.created
|
|
62
301
|
|
|
63
302
|
@property
|
|
@@ -137,11 +376,24 @@ class PublishableEntityMixin(models.Model):
|
|
|
137
376
|
field_to_pev = self.content_version_model_cls._meta.get_field(
|
|
138
377
|
"publishable_entity_version"
|
|
139
378
|
)
|
|
140
|
-
|
|
141
379
|
# Now that we know the field that leads to PublishableEntityVersion,
|
|
142
380
|
# get the reverse related field name so that we can use that later.
|
|
143
381
|
self.related_name = field_to_pev.related_query_name()
|
|
144
382
|
|
|
383
|
+
if field_to_pev.model != self.content_version_model_cls:
|
|
384
|
+
# In the case of multi-table inheritance and mixins, this can get tricky.
|
|
385
|
+
# Example:
|
|
386
|
+
# content_version_model_cls is UnitVersion, which is a subclass of ContainerVersion
|
|
387
|
+
# This versioning helper can be accessed via unit_version.versioning (should return UnitVersion) or
|
|
388
|
+
# via container_version.versioning (should return ContainerVersion)
|
|
389
|
+
intermediate_model = field_to_pev.model # example: ContainerVersion
|
|
390
|
+
# This is the field on the subclass (e.g. UnitVersion) that gets
|
|
391
|
+
# the intermediate (e.g. ContainerVersion). Example: "UnitVersion.container_version" (1:1 foreign key)
|
|
392
|
+
field_to_intermediate = self.content_version_model_cls._meta.get_ancestor_link(intermediate_model)
|
|
393
|
+
if field_to_intermediate:
|
|
394
|
+
# Example: self.related_name = "containerversion.unitversion"
|
|
395
|
+
self.related_name = self.related_name + "." + field_to_intermediate.related_query_name()
|
|
396
|
+
|
|
145
397
|
def _content_obj_version(self, pub_ent_version: PublishableEntityVersion | None):
|
|
146
398
|
"""
|
|
147
399
|
PublishableEntityVersion -> Content object version
|
|
@@ -151,7 +403,10 @@ class PublishableEntityMixin(models.Model):
|
|
|
151
403
|
"""
|
|
152
404
|
if pub_ent_version is None:
|
|
153
405
|
return None
|
|
154
|
-
|
|
406
|
+
obj = pub_ent_version
|
|
407
|
+
for field_name in self.related_name.split("."):
|
|
408
|
+
obj = getattr(obj, field_name)
|
|
409
|
+
return obj
|
|
155
410
|
|
|
156
411
|
@property
|
|
157
412
|
def draft(self):
|
|
@@ -294,36 +549,29 @@ class PublishableEntityVersionMixin(models.Model):
|
|
|
294
549
|
details).
|
|
295
550
|
"""
|
|
296
551
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
.get_queryset()
|
|
302
|
-
.select_related(
|
|
303
|
-
"publishable_entity_version",
|
|
304
|
-
)
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
objects: models.Manager[PublishableEntityVersionMixin] = PublishableEntityVersionMixinManager()
|
|
552
|
+
# select these related entities by default for all queries
|
|
553
|
+
objects: ClassVar[WithRelationsManager[Self]] = WithRelationsManager(
|
|
554
|
+
"publishable_entity_version",
|
|
555
|
+
)
|
|
308
556
|
|
|
309
557
|
publishable_entity_version = models.OneToOneField(
|
|
310
558
|
PublishableEntityVersion, on_delete=models.CASCADE, primary_key=True
|
|
311
559
|
)
|
|
312
560
|
|
|
313
561
|
@property
|
|
314
|
-
def uuid(self):
|
|
562
|
+
def uuid(self) -> str:
|
|
315
563
|
return self.publishable_entity_version.uuid
|
|
316
564
|
|
|
317
565
|
@property
|
|
318
|
-
def title(self):
|
|
566
|
+
def title(self) -> str:
|
|
319
567
|
return self.publishable_entity_version.title
|
|
320
568
|
|
|
321
569
|
@property
|
|
322
|
-
def created(self):
|
|
570
|
+
def created(self) -> datetime:
|
|
323
571
|
return self.publishable_entity_version.created
|
|
324
572
|
|
|
325
573
|
@property
|
|
326
|
-
def version_num(self):
|
|
574
|
+
def version_num(self) -> int:
|
|
327
575
|
return self.publishable_entity_version.version_num
|
|
328
576
|
|
|
329
577
|
class Meta:
|
|
File without changes
|