openedx-learning 0.18.2__py2.py3-none-any.whl → 0.19.0__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 +3 -3
- 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/api.py +459 -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/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} +284 -41
- openedx_learning/apps/authoring/units/__init__.py +0 -0
- openedx_learning/apps/authoring/units/api.py +290 -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.2.dist-info → openedx_learning-0.19.0.dist-info}/METADATA +2 -2
- {openedx_learning-0.18.2.dist-info → openedx_learning-0.19.0.dist-info}/RECORD +35 -23
- {openedx_learning-0.18.2.dist-info → openedx_learning-0.19.0.dist-info}/WHEEL +1 -1
- openedx_tagging/core/tagging/api.py +11 -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.2.dist-info → openedx_learning-0.19.0.dist-info}/LICENSE.txt +0 -0
- {openedx_learning-0.18.2.dist-info → openedx_learning-0.19.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated by Django 4.2.19 on 2025-03-11 04:10
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('oel_publishing', '0002_alter_learningpackage_key_and_more'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name='Container',
|
|
16
|
+
fields=[
|
|
17
|
+
('publishable_entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentity')),
|
|
18
|
+
],
|
|
19
|
+
options={
|
|
20
|
+
'abstract': False,
|
|
21
|
+
},
|
|
22
|
+
),
|
|
23
|
+
migrations.CreateModel(
|
|
24
|
+
name='EntityList',
|
|
25
|
+
fields=[
|
|
26
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
27
|
+
],
|
|
28
|
+
),
|
|
29
|
+
migrations.CreateModel(
|
|
30
|
+
name='EntityListRow',
|
|
31
|
+
fields=[
|
|
32
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
33
|
+
('order_num', models.PositiveIntegerField()),
|
|
34
|
+
('entity', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='oel_publishing.publishableentity')),
|
|
35
|
+
('entity_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.entitylist')),
|
|
36
|
+
('entity_version', models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='+', to='oel_publishing.publishableentityversion')),
|
|
37
|
+
],
|
|
38
|
+
),
|
|
39
|
+
migrations.CreateModel(
|
|
40
|
+
name='ContainerVersion',
|
|
41
|
+
fields=[
|
|
42
|
+
('publishable_entity_version', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentityversion')),
|
|
43
|
+
('container', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='oel_publishing.container')),
|
|
44
|
+
('entity_list', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='container_versions', to='oel_publishing.entitylist')),
|
|
45
|
+
],
|
|
46
|
+
options={
|
|
47
|
+
'abstract': False,
|
|
48
|
+
},
|
|
49
|
+
),
|
|
50
|
+
migrations.AddConstraint(
|
|
51
|
+
model_name='entitylistrow',
|
|
52
|
+
constraint=models.UniqueConstraint(fields=('entity_list', 'order_num'), name='oel_publishing_elist_row_order'),
|
|
53
|
+
),
|
|
54
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
* Hierarchical relationships between "container" entities and their children
|
|
8
|
+
* The management of drafts.
|
|
9
|
+
* Publishing specific versions of publishable entities.
|
|
10
|
+
* Finding the currently published versions.
|
|
11
|
+
* The act of publishing, and doing so atomically.
|
|
12
|
+
* Managing reverts.
|
|
13
|
+
* Storing and querying publish history.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .container import Container, ContainerVersion
|
|
17
|
+
from .draft_published import Draft, Published
|
|
18
|
+
from .entity_list import EntityList, EntityListRow
|
|
19
|
+
from .learning_package import LearningPackage
|
|
20
|
+
from .publish_log import PublishLog, PublishLogRecord
|
|
21
|
+
from .publishable_entity import (
|
|
22
|
+
PublishableContentModelRegistry,
|
|
23
|
+
PublishableEntity,
|
|
24
|
+
PublishableEntityMixin,
|
|
25
|
+
PublishableEntityVersion,
|
|
26
|
+
PublishableEntityVersionMixin,
|
|
27
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Container and ContainerVersion models
|
|
3
|
+
"""
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
from .entity_list import EntityList
|
|
8
|
+
from .publishable_entity import PublishableEntityMixin, PublishableEntityVersionMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Container(PublishableEntityMixin):
|
|
12
|
+
"""
|
|
13
|
+
A Container is a type of PublishableEntity that holds other
|
|
14
|
+
PublishableEntities. For example, a "Unit" Container might hold several
|
|
15
|
+
Components.
|
|
16
|
+
|
|
17
|
+
For now, all containers have a static "entity list" that defines which
|
|
18
|
+
containers/components/enities they hold. As we complete the Containers API,
|
|
19
|
+
we will also add support for dynamic containers which may contain different
|
|
20
|
+
entities for different learners or at different times.
|
|
21
|
+
|
|
22
|
+
NOTE: We're going to want to eventually have some association between the
|
|
23
|
+
PublishLog and Containers that were affected in a publish because their
|
|
24
|
+
child elements were published.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ContainerVersion(PublishableEntityVersionMixin):
|
|
29
|
+
"""
|
|
30
|
+
A version of a Container.
|
|
31
|
+
|
|
32
|
+
By convention, we would only want to create new versions when the Container
|
|
33
|
+
itself changes, and not when the Container's child elements change. For
|
|
34
|
+
example:
|
|
35
|
+
|
|
36
|
+
* Something was added to the Container.
|
|
37
|
+
* We re-ordered the rows in the container.
|
|
38
|
+
* Something was removed to the container.
|
|
39
|
+
* The Container's metadata changed, e.g. the title.
|
|
40
|
+
* We pin to different versions of the Container.
|
|
41
|
+
|
|
42
|
+
The last looks a bit odd, but it's because *how we've defined the Unit* has
|
|
43
|
+
changed if we decide to explicitly pin a set of versions for the children,
|
|
44
|
+
and then later change our minds and move to a different set. It also just
|
|
45
|
+
makes things easier to reason about if we say that entity_list never
|
|
46
|
+
changes for a given ContainerVersion.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
container = models.ForeignKey(
|
|
50
|
+
Container,
|
|
51
|
+
on_delete=models.CASCADE,
|
|
52
|
+
related_name="versions",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# The list of entities (frozen and/or unfrozen) in this container
|
|
56
|
+
entity_list = models.ForeignKey(
|
|
57
|
+
EntityList,
|
|
58
|
+
on_delete=models.RESTRICT,
|
|
59
|
+
null=False,
|
|
60
|
+
related_name="container_versions",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def clean(self):
|
|
64
|
+
"""
|
|
65
|
+
Validate this model before saving. Not called normally, but will be
|
|
66
|
+
called if anything is edited via a ModelForm like the Django admin.
|
|
67
|
+
"""
|
|
68
|
+
super().clean()
|
|
69
|
+
if self.container_id != self.publishable_entity_version.entity.container.pk: # pylint: disable=no-member
|
|
70
|
+
raise ValidationError("Inconsistent foreign keys to Container")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Draft and Published models
|
|
3
|
+
"""
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
from .publish_log import PublishLogRecord
|
|
7
|
+
from .publishable_entity import PublishableEntity, PublishableEntityVersion
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Draft(models.Model):
|
|
11
|
+
"""
|
|
12
|
+
Find the active draft version of an entity (usually most recently created).
|
|
13
|
+
|
|
14
|
+
This model mostly only exists to allow us to join against a bunch of
|
|
15
|
+
PublishableEntity objects at once and get all their latest drafts. You might
|
|
16
|
+
use this together with Published in order to see which Drafts haven't been
|
|
17
|
+
published yet.
|
|
18
|
+
|
|
19
|
+
A Draft entry should be created whenever a new PublishableEntityVersion is
|
|
20
|
+
created. This means there are three possible states:
|
|
21
|
+
|
|
22
|
+
1. No Draft entry for a PublishableEntity: This means a PublishableEntity
|
|
23
|
+
was created, but no PublishableEntityVersion was ever made for it, so
|
|
24
|
+
there was never a Draft version.
|
|
25
|
+
2. A Draft entry exists and points to a PublishableEntityVersion: This is
|
|
26
|
+
the most common state.
|
|
27
|
+
3. A Draft entry exists and points to a null version: This means a version
|
|
28
|
+
used to be the draft, but it's been functionally "deleted". The versions
|
|
29
|
+
still exist in our history, but we're done using it.
|
|
30
|
+
|
|
31
|
+
It would have saved a little space to add this data to the Published model
|
|
32
|
+
(and possibly call the combined model something else). Split Modulestore did
|
|
33
|
+
this with its active_versions table. I keep it separate here to get a better
|
|
34
|
+
separation of lifecycle events: i.e. this table *only* changes when drafts
|
|
35
|
+
are updated, not when publishing happens. The Published model only changes
|
|
36
|
+
when something is published.
|
|
37
|
+
"""
|
|
38
|
+
# If we're removing a PublishableEntity entirely, also remove the Draft
|
|
39
|
+
# entry for it. This isn't a normal operation, but can happen if you're
|
|
40
|
+
# deleting an entire LearningPackage.
|
|
41
|
+
entity = models.OneToOneField(
|
|
42
|
+
PublishableEntity,
|
|
43
|
+
on_delete=models.CASCADE,
|
|
44
|
+
primary_key=True,
|
|
45
|
+
)
|
|
46
|
+
version = models.OneToOneField(
|
|
47
|
+
PublishableEntityVersion,
|
|
48
|
+
on_delete=models.RESTRICT,
|
|
49
|
+
null=True,
|
|
50
|
+
blank=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Published(models.Model):
|
|
55
|
+
"""
|
|
56
|
+
Find the currently published version of an entity.
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
|
|
60
|
+
* There is only ever one published PublishableEntityVersion per
|
|
61
|
+
PublishableEntity at any given time.
|
|
62
|
+
* It may be possible for a PublishableEntity to exist only as a Draft (and thus
|
|
63
|
+
not show up in this table).
|
|
64
|
+
* If a row exists for a PublishableEntity, but the ``version`` field is
|
|
65
|
+
None, it means that the entity was published at some point, but is no
|
|
66
|
+
longer published now–i.e. it's functionally "deleted", even though all
|
|
67
|
+
the version history is preserved behind the scenes.
|
|
68
|
+
|
|
69
|
+
TODO: Do we need to create a (redundant) title field in this model so that
|
|
70
|
+
we can more efficiently search across titles within a LearningPackage?
|
|
71
|
+
Probably not an immediate concern because the number of rows currently
|
|
72
|
+
shouldn't be > 10,000 in the more extreme cases.
|
|
73
|
+
|
|
74
|
+
TODO: Do we need to make a "most_recently" published version when an entry
|
|
75
|
+
is unpublished/deleted?
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
entity = models.OneToOneField(
|
|
79
|
+
PublishableEntity,
|
|
80
|
+
on_delete=models.CASCADE,
|
|
81
|
+
primary_key=True,
|
|
82
|
+
)
|
|
83
|
+
version = models.OneToOneField(
|
|
84
|
+
PublishableEntityVersion,
|
|
85
|
+
on_delete=models.RESTRICT,
|
|
86
|
+
null=True,
|
|
87
|
+
)
|
|
88
|
+
publish_log_record = models.ForeignKey(
|
|
89
|
+
PublishLogRecord,
|
|
90
|
+
on_delete=models.RESTRICT,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
verbose_name = "Published Entity"
|
|
95
|
+
verbose_name_plural = "Published Entities"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entity List models
|
|
3
|
+
"""
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
from .publishable_entity import PublishableEntity, PublishableEntityVersion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EntityList(models.Model):
|
|
10
|
+
"""
|
|
11
|
+
EntityLists are a common structure to hold parent-child relations.
|
|
12
|
+
|
|
13
|
+
EntityLists are not PublishableEntities in and of themselves. That's because
|
|
14
|
+
sometimes we'll want the same kind of data structure for things that we
|
|
15
|
+
dynamically generate for individual students (e.g. Variants). EntityLists are
|
|
16
|
+
anonymous in a sense–they're pointed to by ContainerVersions and
|
|
17
|
+
other models, rather than being looked up by their own identifiers.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EntityListRow(models.Model):
|
|
22
|
+
"""
|
|
23
|
+
Each EntityListRow points to a PublishableEntity, optionally at a specific
|
|
24
|
+
version.
|
|
25
|
+
|
|
26
|
+
There is a row in this table for each member of an EntityList. The order_num
|
|
27
|
+
field is used to determine the order of the members in the list.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
entity_list = models.ForeignKey(EntityList, on_delete=models.CASCADE)
|
|
31
|
+
|
|
32
|
+
# This ordering should be treated as immutable–if the ordering needs to
|
|
33
|
+
# change, we create a new EntityList and copy things over.
|
|
34
|
+
order_num = models.PositiveIntegerField()
|
|
35
|
+
|
|
36
|
+
# Simple case would use these fields with our convention that null versions
|
|
37
|
+
# means "get the latest draft or published as appropriate". These entities
|
|
38
|
+
# could be Selectors, in which case we'd need to do more work to find the right
|
|
39
|
+
# variant. The publishing app itself doesn't know anything about Selectors
|
|
40
|
+
# however, and just treats it as another PublishableEntity.
|
|
41
|
+
entity = models.ForeignKey(PublishableEntity, on_delete=models.RESTRICT)
|
|
42
|
+
|
|
43
|
+
# The version references point to the specific PublishableEntityVersion that
|
|
44
|
+
# this EntityList has for this PublishableEntity for both the draft and
|
|
45
|
+
# published states. However, we don't want to have to create new EntityList
|
|
46
|
+
# every time that a member is updated, because that would waste a lot of
|
|
47
|
+
# space and make it difficult to figure out when the metadata of something
|
|
48
|
+
# like a Unit *actually* changed, vs. when its child members were being
|
|
49
|
+
# updated. Doing so could also potentially lead to race conditions when
|
|
50
|
+
# updating multiple layers of containers.
|
|
51
|
+
#
|
|
52
|
+
# So our approach to this is to use a value of None (null) to represent an
|
|
53
|
+
# unpinned reference to a PublishableEntity. It's shorthand for "just use
|
|
54
|
+
# the latest draft or published version of this, as appropriate".
|
|
55
|
+
entity_version = models.ForeignKey(
|
|
56
|
+
PublishableEntityVersion,
|
|
57
|
+
on_delete=models.RESTRICT,
|
|
58
|
+
null=True,
|
|
59
|
+
related_name="+", # Do we need the reverse relation?
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
constraints = [
|
|
64
|
+
# If (entity_list, order_num) is not unique, it likely indicates a race condition - so force uniqueness.
|
|
65
|
+
models.UniqueConstraint(
|
|
66
|
+
fields=["entity_list", "order_num"],
|
|
67
|
+
name="oel_publishing_elist_row_order",
|
|
68
|
+
),
|
|
69
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LearningPackage model
|
|
3
|
+
"""
|
|
4
|
+
from django.db import models
|
|
5
|
+
|
|
6
|
+
from openedx_learning.lib.fields import (
|
|
7
|
+
MultiCollationTextField,
|
|
8
|
+
case_insensitive_char_field,
|
|
9
|
+
immutable_uuid_field,
|
|
10
|
+
key_field,
|
|
11
|
+
manual_date_time_field,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LearningPackage(models.Model):
|
|
16
|
+
"""
|
|
17
|
+
Top level container for a grouping of authored content.
|
|
18
|
+
|
|
19
|
+
Each PublishableEntity belongs to exactly one LearningPackage.
|
|
20
|
+
"""
|
|
21
|
+
# Explictly declare a 4-byte ID instead of using the app-default 8-byte ID.
|
|
22
|
+
# We do not expect to have more than 2 billion LearningPackages on a given
|
|
23
|
+
# site. Furthermore, many, many things have foreign keys to this model and
|
|
24
|
+
# uniqueness indexes on those foreign keys + their own fields, so the 4
|
|
25
|
+
# bytes saved will add up over time.
|
|
26
|
+
id = models.AutoField(primary_key=True)
|
|
27
|
+
|
|
28
|
+
uuid = immutable_uuid_field()
|
|
29
|
+
|
|
30
|
+
# "key" is a reserved word for MySQL, so we're temporarily using the column
|
|
31
|
+
# name of "_key" to avoid breaking downstream tooling. There's an open
|
|
32
|
+
# question as to whether this field needs to exist at all, or whether the
|
|
33
|
+
# top level library key it's currently used for should be entirely in the
|
|
34
|
+
# LibraryContent model.
|
|
35
|
+
key = key_field(db_column="_key")
|
|
36
|
+
|
|
37
|
+
title = case_insensitive_char_field(max_length=500, blank=False)
|
|
38
|
+
|
|
39
|
+
# TODO: We should probably defer this field, since many things pull back
|
|
40
|
+
# LearningPackage as select_related. Usually those relations only care about
|
|
41
|
+
# the UUID and key, so maybe it makes sense to separate the model at some
|
|
42
|
+
# point.
|
|
43
|
+
description = MultiCollationTextField(
|
|
44
|
+
blank=True,
|
|
45
|
+
null=False,
|
|
46
|
+
default="",
|
|
47
|
+
max_length=10_000,
|
|
48
|
+
# We don't really expect to ever sort by the text column, but we may
|
|
49
|
+
# want to do case-insensitive searches, so it's useful to have a case
|
|
50
|
+
# and accent insensitive collation.
|
|
51
|
+
db_collations={
|
|
52
|
+
"sqlite": "NOCASE",
|
|
53
|
+
"mysql": "utf8mb4_unicode_ci",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
created = manual_date_time_field()
|
|
58
|
+
updated = manual_date_time_field()
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return f"{self.key}"
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
constraints = [
|
|
65
|
+
# LearningPackage keys must be globally unique. This is something
|
|
66
|
+
# that might be relaxed in the future if this system were to be
|
|
67
|
+
# extensible to something like multi-tenancy, in which case we'd tie
|
|
68
|
+
# it to something like a Site or Org.
|
|
69
|
+
models.UniqueConstraint(
|
|
70
|
+
fields=["key"],
|
|
71
|
+
name="oel_publishing_lp_uniq_key",
|
|
72
|
+
)
|
|
73
|
+
]
|
|
74
|
+
verbose_name = "Learning Package"
|
|
75
|
+
verbose_name_plural = "Learning Packages"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PublishLog and PublishLogRecord models
|
|
3
|
+
"""
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
from openedx_learning.lib.fields import case_insensitive_char_field, immutable_uuid_field, manual_date_time_field
|
|
8
|
+
|
|
9
|
+
from .learning_package import LearningPackage
|
|
10
|
+
from .publishable_entity import PublishableEntity, PublishableEntityVersion
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PublishLog(models.Model):
|
|
14
|
+
"""
|
|
15
|
+
There is one row in this table for every time content is published.
|
|
16
|
+
|
|
17
|
+
Each PublishLog has 0 or more PublishLogRecords describing exactly which
|
|
18
|
+
PublishableEntites were published and what the version changes are. A
|
|
19
|
+
PublishLog is like a git commit in that sense, with individual
|
|
20
|
+
PublishLogRecords representing the files changed.
|
|
21
|
+
|
|
22
|
+
Open question: Empty publishes are allowed at this time, and might be useful
|
|
23
|
+
for "fake" publishes that are necessary to invoke other post-publish
|
|
24
|
+
actions. It's not clear at this point how useful this will actually be.
|
|
25
|
+
|
|
26
|
+
The absence of a ``version_num`` field in this model is intentional, because
|
|
27
|
+
having one would potentially cause write contention/locking issues when
|
|
28
|
+
there are many people working on different entities in a very large library.
|
|
29
|
+
We already see some contention issues occuring in ModuleStore for courses,
|
|
30
|
+
and we want to support Libraries that are far larger.
|
|
31
|
+
|
|
32
|
+
If you need a LearningPackage-wide indicator for version and the only thing
|
|
33
|
+
you care about is "has *something* changed?", you can make a foreign key to
|
|
34
|
+
the most recent PublishLog, or use the most recent PublishLog's primary key.
|
|
35
|
+
This should be monotonically increasing, though there will be large gaps in
|
|
36
|
+
values, e.g. (5, 190, 1291, etc.). Be warned that this value will not port
|
|
37
|
+
across sites. If you need site-portability, the UUIDs for this model are a
|
|
38
|
+
safer bet, though there's a lot about import/export that we haven't fully
|
|
39
|
+
mapped out yet.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
uuid = immutable_uuid_field()
|
|
43
|
+
learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)
|
|
44
|
+
message = case_insensitive_char_field(max_length=500, blank=True, default="")
|
|
45
|
+
published_at = manual_date_time_field()
|
|
46
|
+
published_by = models.ForeignKey(
|
|
47
|
+
settings.AUTH_USER_MODEL,
|
|
48
|
+
on_delete=models.SET_NULL,
|
|
49
|
+
null=True,
|
|
50
|
+
blank=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
verbose_name = "Publish Log"
|
|
55
|
+
verbose_name_plural = "Publish Logs"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PublishLogRecord(models.Model):
|
|
59
|
+
"""
|
|
60
|
+
A record for each publishable entity version changed, for each publish.
|
|
61
|
+
|
|
62
|
+
To revert a publish, we would make a new publish that swaps ``old_version``
|
|
63
|
+
and ``new_version`` field values.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
publish_log = models.ForeignKey(
|
|
67
|
+
PublishLog,
|
|
68
|
+
on_delete=models.CASCADE,
|
|
69
|
+
related_name="records",
|
|
70
|
+
)
|
|
71
|
+
entity = models.ForeignKey(PublishableEntity, on_delete=models.RESTRICT)
|
|
72
|
+
old_version = models.ForeignKey(
|
|
73
|
+
PublishableEntityVersion,
|
|
74
|
+
on_delete=models.RESTRICT,
|
|
75
|
+
null=True,
|
|
76
|
+
blank=True,
|
|
77
|
+
related_name="+",
|
|
78
|
+
)
|
|
79
|
+
new_version = models.ForeignKey(
|
|
80
|
+
PublishableEntityVersion, on_delete=models.RESTRICT, null=True, blank=True
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
class Meta:
|
|
84
|
+
constraints = [
|
|
85
|
+
# A Publishable can have only one PublishLogRecord per PublishLog.
|
|
86
|
+
# You can't simultaneously publish two different versions of the
|
|
87
|
+
# same publishable.
|
|
88
|
+
models.UniqueConstraint(
|
|
89
|
+
fields=[
|
|
90
|
+
"publish_log",
|
|
91
|
+
"entity",
|
|
92
|
+
],
|
|
93
|
+
name="oel_plr_uniq_pl_publishable",
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
indexes = [
|
|
97
|
+
# Publishable (reverse) Publish Log Index:
|
|
98
|
+
# * Find the history of publishes for a given Publishable,
|
|
99
|
+
# starting with the most recent (since IDs are ascending ints).
|
|
100
|
+
models.Index(
|
|
101
|
+
fields=["entity", "-publish_log"],
|
|
102
|
+
name="oel_plr_idx_entity_rplr",
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
verbose_name = "Publish Log Record"
|
|
106
|
+
verbose_name_plural = "Publish Log Records"
|