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.
Files changed (38) hide show
  1. openedx_learning/__init__.py +1 -1
  2. openedx_learning/api/authoring.py +1 -0
  3. openedx_learning/api/authoring_models.py +1 -1
  4. openedx_learning/apps/authoring/components/api.py +18 -5
  5. openedx_learning/apps/authoring/components/apps.py +1 -1
  6. openedx_learning/apps/authoring/components/models.py +9 -14
  7. openedx_learning/apps/authoring/contents/models.py +1 -1
  8. openedx_learning/apps/authoring/publishing/admin.py +3 -0
  9. openedx_learning/apps/authoring/publishing/api.py +508 -3
  10. openedx_learning/apps/authoring/publishing/apps.py +9 -0
  11. openedx_learning/apps/authoring/publishing/migrations/0003_containers.py +54 -0
  12. openedx_learning/apps/authoring/publishing/migrations/0004_publishableentity_can_stand_alone.py +21 -0
  13. openedx_learning/apps/authoring/publishing/models/__init__.py +27 -0
  14. openedx_learning/apps/authoring/publishing/models/container.py +70 -0
  15. openedx_learning/apps/authoring/publishing/models/draft_published.py +95 -0
  16. openedx_learning/apps/authoring/publishing/models/entity_list.py +69 -0
  17. openedx_learning/apps/authoring/publishing/models/learning_package.py +75 -0
  18. openedx_learning/apps/authoring/publishing/models/publish_log.py +106 -0
  19. openedx_learning/apps/authoring/publishing/{model_mixins.py → models/publishable_entity.py} +289 -41
  20. openedx_learning/apps/authoring/units/__init__.py +0 -0
  21. openedx_learning/apps/authoring/units/api.py +305 -0
  22. openedx_learning/apps/authoring/units/apps.py +25 -0
  23. openedx_learning/apps/authoring/units/migrations/0001_initial.py +36 -0
  24. openedx_learning/apps/authoring/units/migrations/__init__.py +0 -0
  25. openedx_learning/apps/authoring/units/models.py +50 -0
  26. openedx_learning/contrib/media_server/apps.py +1 -1
  27. openedx_learning/lib/managers.py +7 -1
  28. openedx_learning/lib/validators.py +1 -1
  29. {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/METADATA +3 -3
  30. {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/RECORD +37 -24
  31. {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/WHEEL +1 -1
  32. openedx_tagging/core/tagging/api.py +4 -4
  33. openedx_tagging/core/tagging/models/base.py +1 -1
  34. openedx_tagging/core/tagging/rest_api/v1/permissions.py +1 -1
  35. openedx_tagging/core/tagging/rules.py +1 -2
  36. openedx_learning/apps/authoring/publishing/models.py +0 -517
  37. {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/LICENSE.txt +0 -0
  38. {openedx_learning-0.18.3.dist-info → openedx_learning-0.19.1.dist-info}/top_level.txt +0 -0
@@ -6,8 +6,7 @@ from __future__ import annotations
6
6
  from typing import Callable, Union
7
7
 
8
8
  import django.contrib.auth.models
9
- # typing support in rules depends on https://github.com/dfunckt/django-rules/pull/177
10
- import rules # type: ignore[import]
9
+ import rules
11
10
  from attrs import define
12
11
 
13
12
  from .models import Tag, Taxonomy
@@ -1,517 +0,0 @@
1
- """
2
- The data models here are intended to be used by other apps to publish different
3
- types of content, such as Components, Units, Sections, etc. These models should
4
- support the logic for the management of the publishing process:
5
-
6
- * The relationship between publishable entities and their many versions.
7
- * The management of drafts.
8
- * Publishing specific versions of publishable entities.
9
- * Finding the currently published versions.
10
- * The act of publishing, and doing so atomically.
11
- * Managing reverts.
12
- * Storing and querying publish history.
13
- """
14
- from django.conf import settings
15
- from django.core.validators import MinValueValidator
16
- from django.db import models
17
-
18
- from openedx_learning.lib.fields import (
19
- MultiCollationTextField,
20
- case_insensitive_char_field,
21
- immutable_uuid_field,
22
- key_field,
23
- manual_date_time_field,
24
- )
25
-
26
- __all__ = [
27
- "LearningPackage",
28
- "PublishableEntity",
29
- "PublishableEntityVersion",
30
- "Draft",
31
- "PublishLog",
32
- "PublishLogRecord",
33
- "Published",
34
- ]
35
-
36
-
37
- class LearningPackage(models.Model): # type: ignore[django-manager-missing]
38
- """
39
- Top level container for a grouping of authored content.
40
-
41
- Each PublishableEntity belongs to exactly one LearningPackage.
42
- """
43
- # Explictly declare a 4-byte ID instead of using the app-default 8-byte ID.
44
- # We do not expect to have more than 2 billion LearningPackages on a given
45
- # site. Furthermore, many, many things have foreign keys to this model and
46
- # uniqueness indexes on those foreign keys + their own fields, so the 4
47
- # bytes saved will add up over time.
48
- id = models.AutoField(primary_key=True)
49
-
50
- uuid = immutable_uuid_field()
51
-
52
- # "key" is a reserved word for MySQL, so we're temporarily using the column
53
- # name of "_key" to avoid breaking downstream tooling. There's an open
54
- # question as to whether this field needs to exist at all, or whether the
55
- # top level library key it's currently used for should be entirely in the
56
- # LibraryContent model.
57
- key = key_field(db_column="_key")
58
-
59
- title = case_insensitive_char_field(max_length=500, blank=False)
60
-
61
- # TODO: We should probably defer this field, since many things pull back
62
- # LearningPackage as select_related. Usually those relations only care about
63
- # the UUID and key, so maybe it makes sense to separate the model at some
64
- # point.
65
- description = MultiCollationTextField(
66
- blank=True,
67
- null=False,
68
- default="",
69
- max_length=10_000,
70
- # We don't really expect to ever sort by the text column, but we may
71
- # want to do case-insensitive searches, so it's useful to have a case
72
- # and accent insensitive collation.
73
- db_collations={
74
- "sqlite": "NOCASE",
75
- "mysql": "utf8mb4_unicode_ci",
76
- }
77
- )
78
-
79
- created = manual_date_time_field()
80
- updated = manual_date_time_field()
81
-
82
- def __str__(self):
83
- return f"{self.key}"
84
-
85
- class Meta:
86
- constraints = [
87
- # LearningPackage keys must be globally unique. This is something
88
- # that might be relaxed in the future if this system were to be
89
- # extensible to something like multi-tenancy, in which case we'd tie
90
- # it to something like a Site or Org.
91
- models.UniqueConstraint(
92
- fields=["key"],
93
- name="oel_publishing_lp_uniq_key",
94
- )
95
- ]
96
- verbose_name = "Learning Package"
97
- verbose_name_plural = "Learning Packages"
98
-
99
-
100
- class PublishableEntity(models.Model):
101
- """
102
- This represents any publishable thing that has ever existed in a
103
- LearningPackage. It serves as a stable model that will not go away even if
104
- these things are later unpublished or deleted.
105
-
106
- A PublishableEntity belongs to exactly one LearningPackage.
107
-
108
- Examples of Publishable Entities
109
- --------------------------------
110
-
111
- Components (e.g. VideoBlock, ProblemBlock), Units, and Sections/Subsections
112
- would all be considered Publishable Entites. But anything that can be
113
- imported, exported, published, and reverted in a course or library could be
114
- modeled as a PublishableEntity, including things like Grading Policy or
115
- possibly Taxonomies (?).
116
-
117
- How to use this model
118
- ---------------------
119
-
120
- The publishing app understands that publishable entities exist, along with
121
- their drafts and published versions. It has some basic metadata, such as
122
- identifiers, who created it, and when it was created. It's meant to
123
- encapsulate the draft and publishing related aspects of your content, but
124
- the ``publishing`` app doesn't know anything about the actual content being
125
- referenced.
126
-
127
- You have to provide actual meaning to PublishableEntity by creating your own
128
- models that will represent your particular content and associating them to
129
- PublishableEntity via a OneToOneField with primary_key=True. The easiest way
130
- to do this is to have your model inherit from PublishableEntityMixin.
131
-
132
- Identifiers
133
- -----------
134
- The UUID is globally unique and should be treated as immutable.
135
-
136
- The key field *is* mutable, but changing it will affect all
137
- PublishedEntityVersions. They are locally unique within the LearningPackage.
138
-
139
- If you are referencing this model from within the same process, use a
140
- foreign key to the id. If you are referencing this PublishedEntity from an
141
- external system/service, use the UUID. The key is the part that is most
142
- likely to be human-readable, and may be exported/copied, but try not to rely
143
- on it, since this value may change.
144
-
145
- Note: When we actually implement the ability to change identifiers, we
146
- should make a history table and a modified attribute on this model.
147
-
148
- Why are Identifiers in this Model?
149
- ----------------------------------
150
-
151
- A PublishableEntity never stands alone–it's always intended to be used with
152
- a 1:1 model like Component or Unit. So why have all the identifiers in this
153
- model instead of storing them in those other models? Two reasons:
154
-
155
- * Published things need to have the right identifiers so they can be used
156
- throughout the system, and the UUID is serving the role of ISBN in physical
157
- book publishing.
158
- * We want to be able to enforce the idea that "key" is locally unique across
159
- all PublishableEntities within a given LearningPackage. Component and Unit
160
- can't do that without a shared model.
161
-
162
- That being said, models that build on PublishableEntity are free to add
163
- their own identifiers if it's useful to do so.
164
-
165
- Why not Inherit from this Model?
166
- --------------------------------
167
-
168
- Django supports multi-table inheritance:
169
-
170
- https://docs.djangoproject.com/en/4.2/topics/db/models/#multi-table-inheritance
171
-
172
- We don't use that, primarily because we want to more clearly decouple
173
- publishing concerns from the rest of the logic around Components, Units,
174
- etc. If you made a Component and ComponentVersion models that subclassed
175
- PublishableEntity and PublishableEntityVersion, and then accessed
176
- ``component.versions``, you might expect ComponentVersions to come back and
177
- be surprised when you get EntityVersions instead.
178
-
179
- In general, we want freedom to add new Publishing models, fields, and
180
- methods without having to worry about the downstream name collisions with
181
- other apps (many of which might live in other repositories). The helper
182
- mixins will provide a little syntactic sugar to make common access patterns
183
- more convenient, like file access.
184
- """
185
-
186
- uuid = immutable_uuid_field()
187
- learning_package = models.ForeignKey(
188
- LearningPackage,
189
- on_delete=models.CASCADE,
190
- related_name="publishable_entities",
191
- )
192
-
193
- # "key" is a reserved word for MySQL, so we're temporarily using the column
194
- # name of "_key" to avoid breaking downstream tooling. Consider renaming
195
- # this later.
196
- key = key_field(db_column="_key")
197
-
198
- created = manual_date_time_field()
199
- created_by = models.ForeignKey(
200
- settings.AUTH_USER_MODEL,
201
- on_delete=models.SET_NULL,
202
- null=True,
203
- blank=True,
204
- )
205
-
206
- class Meta:
207
- constraints = [
208
- # Keys are unique within a given LearningPackage.
209
- models.UniqueConstraint(
210
- fields=[
211
- "learning_package",
212
- "key",
213
- ],
214
- name="oel_pub_ent_uniq_lp_key",
215
- )
216
- ]
217
- indexes = [
218
- # Global Key Index:
219
- # * Search by key across all PublishableEntities on the site. This
220
- # would be a support-oriented tool from Django Admin.
221
- models.Index(
222
- fields=["key"],
223
- name="oel_pub_ent_idx_key",
224
- ),
225
- # LearningPackage (reverse) Created Index:
226
- # * Search for most recently *created* PublishableEntities for a
227
- # given LearningPackage, since they're the most likely to be
228
- # actively worked on.
229
- models.Index(
230
- fields=["learning_package", "-created"],
231
- name="oel_pub_ent_idx_lp_rcreated",
232
- ),
233
- ]
234
- # These are for the Django Admin UI.
235
- verbose_name = "Publishable Entity"
236
- verbose_name_plural = "Publishable Entities"
237
-
238
- def __str__(self):
239
- return f"{self.key}"
240
-
241
-
242
- class PublishableEntityVersion(models.Model):
243
- """
244
- A particular version of a PublishableEntity.
245
-
246
- This model has its own ``uuid`` so that it can be referenced directly. The
247
- ``uuid`` should be treated as immutable.
248
-
249
- PublishableEntityVersions are created once and never updated. So for
250
- instance, the ``title`` should never be modified.
251
-
252
- Like PublishableEntity, the data in this model is only enough to cover the
253
- parts that are most important for the actual process of managing drafts and
254
- publishes. You will want to create your own models to represent the actual
255
- content data that's associated with this PublishableEntityVersion, and
256
- connect them using a OneToOneField with primary_key=True. The easiest way to
257
- do this is to inherit from PublishableEntityVersionMixin. Be sure to treat
258
- these versioned models in your app as immutable as well.
259
- """
260
-
261
- uuid = immutable_uuid_field()
262
- entity = models.ForeignKey(
263
- PublishableEntity, on_delete=models.CASCADE, related_name="versions"
264
- )
265
-
266
- # Most publishable things will have some sort of title, but blanks are
267
- # allowed for those that don't require one.
268
- title = case_insensitive_char_field(max_length=500, blank=True, default="")
269
-
270
- # The version_num starts at 1 and increments by 1 with each new version for
271
- # a given PublishableEntity. Doing it this way makes it more convenient for
272
- # users to refer to than a hash or UUID value. It also helps us catch race
273
- # conditions on save, by setting a unique constraint on the entity and
274
- # version_num.
275
- version_num = models.PositiveIntegerField(
276
- null=False,
277
- validators=[MinValueValidator(1)],
278
- )
279
-
280
- # All PublishableEntityVersions created as part of the same publish should
281
- # have the exact same created datetime (not off by a handful of
282
- # microseconds).
283
- created = manual_date_time_field()
284
-
285
- # User who created the PublishableEntityVersion. This can be null if the
286
- # user is later removed. Open edX in general doesn't let you remove users,
287
- # but we should try to model it so that this is possible eventually.
288
- created_by = models.ForeignKey(
289
- settings.AUTH_USER_MODEL,
290
- on_delete=models.SET_NULL,
291
- null=True,
292
- blank=True,
293
- )
294
-
295
- class Meta:
296
- constraints = [
297
- # Prevent the situation where we have multiple
298
- # PublishableEntityVersions claiming to be the same version_num for
299
- # a given PublishableEntity. This can happen if there's a race
300
- # condition between concurrent editors in different browsers,
301
- # working on the same Publishable. With this constraint, one of
302
- # those processes will raise an IntegrityError.
303
- models.UniqueConstraint(
304
- fields=[
305
- "entity",
306
- "version_num",
307
- ],
308
- name="oel_pv_uniq_entity_version_num",
309
- )
310
- ]
311
- indexes = [
312
- # LearningPackage (reverse) Created Index:
313
- # * Make it cheap to find the most recently created
314
- # PublishableEntityVersions for a given LearningPackage. This
315
- # represents the most recently saved work for a LearningPackage
316
- # and would be the most likely areas to get worked on next.
317
- models.Index(
318
- fields=["entity", "-created"],
319
- name="oel_pv_idx_entity_rcreated",
320
- ),
321
- # Title Index:
322
- # * Search by title.
323
- models.Index(
324
- fields=[
325
- "title",
326
- ],
327
- name="oel_pv_idx_title",
328
- ),
329
- ]
330
-
331
- # These are for the Django Admin UI.
332
- verbose_name = "Publishable Entity Version"
333
- verbose_name_plural = "Publishable Entity Versions"
334
-
335
-
336
- class Draft(models.Model):
337
- """
338
- Find the active draft version of an entity (usually most recently created).
339
-
340
- This model mostly only exists to allow us to join against a bunch of
341
- PublishableEntity objects at once and get all their latest drafts. You might
342
- use this together with Published in order to see which Drafts haven't been
343
- published yet.
344
-
345
- A Draft entry should be created whenever a new PublishableEntityVersion is
346
- created. This means there are three possible states:
347
-
348
- 1. No Draft entry for a PublishableEntity: This means a PublishableEntity
349
- was created, but no PublishableEntityVersion was ever made for it, so
350
- there was never a Draft version.
351
- 2. A Draft entry exists and points to a PublishableEntityVersion: This is
352
- the most common state.
353
- 3. A Draft entry exists and points to a null version: This means a version
354
- used to be the draft, but it's been functionally "deleted". The versions
355
- still exist in our history, but we're done using it.
356
-
357
- It would have saved a little space to add this data to the Published model
358
- (and possibly call the combined model something else). Split Modulestore did
359
- this with its active_versions table. I keep it separate here to get a better
360
- separation of lifecycle events: i.e. this table *only* changes when drafts
361
- are updated, not when publishing happens. The Published model only changes
362
- when something is published.
363
- """
364
- # If we're removing a PublishableEntity entirely, also remove the Draft
365
- # entry for it. This isn't a normal operation, but can happen if you're
366
- # deleting an entire LearningPackage.
367
- entity = models.OneToOneField(
368
- PublishableEntity,
369
- on_delete=models.CASCADE,
370
- primary_key=True,
371
- )
372
- version = models.OneToOneField(
373
- PublishableEntityVersion,
374
- on_delete=models.RESTRICT,
375
- null=True,
376
- blank=True,
377
- )
378
-
379
-
380
- class PublishLog(models.Model):
381
- """
382
- There is one row in this table for every time content is published.
383
-
384
- Each PublishLog has 0 or more PublishLogRecords describing exactly which
385
- PublishableEntites were published and what the version changes are. A
386
- PublishLog is like a git commit in that sense, with individual
387
- PublishLogRecords representing the files changed.
388
-
389
- Open question: Empty publishes are allowed at this time, and might be useful
390
- for "fake" publishes that are necessary to invoke other post-publish
391
- actions. It's not clear at this point how useful this will actually be.
392
-
393
- The absence of a ``version_num`` field in this model is intentional, because
394
- having one would potentially cause write contention/locking issues when
395
- there are many people working on different entities in a very large library.
396
- We already see some contention issues occuring in ModuleStore for courses,
397
- and we want to support Libraries that are far larger.
398
-
399
- If you need a LearningPackage-wide indicator for version and the only thing
400
- you care about is "has *something* changed?", you can make a foreign key to
401
- the most recent PublishLog, or use the most recent PublishLog's primary key.
402
- This should be monotonically increasing, though there will be large gaps in
403
- values, e.g. (5, 190, 1291, etc.). Be warned that this value will not port
404
- across sites. If you need site-portability, the UUIDs for this model are a
405
- safer bet, though there's a lot about import/export that we haven't fully
406
- mapped out yet.
407
- """
408
-
409
- uuid = immutable_uuid_field()
410
- learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
411
- message = case_insensitive_char_field(max_length=500, blank=True, default="")
412
- published_at = manual_date_time_field()
413
- published_by = models.ForeignKey(
414
- settings.AUTH_USER_MODEL,
415
- on_delete=models.SET_NULL,
416
- null=True,
417
- blank=True,
418
- )
419
-
420
- class Meta:
421
- verbose_name = "Publish Log"
422
- verbose_name_plural = "Publish Logs"
423
-
424
-
425
- class PublishLogRecord(models.Model):
426
- """
427
- A record for each publishable entity version changed, for each publish.
428
-
429
- To revert a publish, we would make a new publish that swaps ``old_version``
430
- and ``new_version`` field values.
431
- """
432
-
433
- publish_log = models.ForeignKey(
434
- PublishLog,
435
- on_delete=models.CASCADE,
436
- related_name="records",
437
- )
438
- entity = models.ForeignKey(PublishableEntity, on_delete=models.RESTRICT)
439
- old_version = models.ForeignKey(
440
- PublishableEntityVersion,
441
- on_delete=models.RESTRICT,
442
- null=True,
443
- blank=True,
444
- related_name="+",
445
- )
446
- new_version = models.ForeignKey(
447
- PublishableEntityVersion, on_delete=models.RESTRICT, null=True, blank=True
448
- )
449
-
450
- class Meta:
451
- constraints = [
452
- # A Publishable can have only one PublishLogRecord per PublishLog.
453
- # You can't simultaneously publish two different versions of the
454
- # same publishable.
455
- models.UniqueConstraint(
456
- fields=[
457
- "publish_log",
458
- "entity",
459
- ],
460
- name="oel_plr_uniq_pl_publishable",
461
- )
462
- ]
463
- indexes = [
464
- # Publishable (reverse) Publish Log Index:
465
- # * Find the history of publishes for a given Publishable,
466
- # starting with the most recent (since IDs are ascending ints).
467
- models.Index(
468
- fields=["entity", "-publish_log"],
469
- name="oel_plr_idx_entity_rplr",
470
- ),
471
- ]
472
- verbose_name = "Publish Log Record"
473
- verbose_name_plural = "Publish Log Records"
474
-
475
-
476
- class Published(models.Model):
477
- """
478
- Find the currently published version of an entity.
479
-
480
- Notes:
481
-
482
- * There is only ever one published PublishableEntityVersion per
483
- PublishableEntity at any given time.
484
- * It may be possible for a PublishableEntity to exist only as a Draft (and thus
485
- not show up in this table).
486
- * If a row exists for a PublishableEntity, but the ``version`` field is
487
- None, it means that the entity was published at some point, but is no
488
- longer published now–i.e. it's functionally "deleted", even though all
489
- the version history is preserved behind the scenes.
490
-
491
- TODO: Do we need to create a (redundant) title field in this model so that
492
- we can more efficiently search across titles within a LearningPackage?
493
- Probably not an immediate concern because the number of rows currently
494
- shouldn't be > 10,000 in the more extreme cases.
495
-
496
- TODO: Do we need to make a "most_recently" published version when an entry
497
- is unpublished/deleted?
498
- """
499
-
500
- entity = models.OneToOneField(
501
- PublishableEntity,
502
- on_delete=models.CASCADE,
503
- primary_key=True,
504
- )
505
- version = models.OneToOneField(
506
- PublishableEntityVersion,
507
- on_delete=models.RESTRICT,
508
- null=True,
509
- )
510
- publish_log_record = models.ForeignKey(
511
- PublishLogRecord,
512
- on_delete=models.RESTRICT,
513
- )
514
-
515
- class Meta:
516
- verbose_name = "Published Entity"
517
- verbose_name_plural = "Published Entities"