openedx-learning 0.22.0__py2.py3-none-any.whl → 0.23.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/apps/authoring/publishing/admin.py +76 -1
- openedx_learning/apps/authoring/publishing/api.py +333 -24
- openedx_learning/apps/authoring/publishing/contextmanagers.py +157 -0
- openedx_learning/apps/authoring/publishing/migrations/0006_draftchangelog.py +68 -0
- openedx_learning/apps/authoring/publishing/migrations/0007_bootstrap_draftchangelog.py +94 -0
- openedx_learning/apps/authoring/publishing/migrations/0008_alter_draftchangelogrecord_options_and_more.py +32 -0
- openedx_learning/apps/authoring/publishing/models/__init__.py +2 -2
- openedx_learning/apps/authoring/publishing/models/draft_log.py +315 -0
- openedx_learning/apps/authoring/publishing/models/publish_log.py +44 -0
- {openedx_learning-0.22.0.dist-info → openedx_learning-0.23.1.dist-info}/METADATA +4 -4
- {openedx_learning-0.22.0.dist-info → openedx_learning-0.23.1.dist-info}/RECORD +15 -11
- openedx_learning/apps/authoring/publishing/models/draft_published.py +0 -95
- {openedx_learning-0.22.0.dist-info → openedx_learning-0.23.1.dist-info}/WHEEL +0 -0
- {openedx_learning-0.22.0.dist-info → openedx_learning-0.23.1.dist-info}/licenses/LICENSE.txt +0 -0
- {openedx_learning-0.22.0.dist-info → openedx_learning-0.23.1.dist-info}/top_level.txt +0 -0
openedx_learning/__init__.py
CHANGED
|
@@ -4,10 +4,19 @@ Django admin for publishing models
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from django.contrib import admin
|
|
7
|
+
from django.db.models import Count
|
|
7
8
|
|
|
8
9
|
from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, one_to_one_related_model_html
|
|
9
10
|
|
|
10
|
-
from .models import
|
|
11
|
+
from .models import (
|
|
12
|
+
DraftChangeLog,
|
|
13
|
+
DraftChangeLogRecord,
|
|
14
|
+
LearningPackage,
|
|
15
|
+
PublishableEntity,
|
|
16
|
+
PublishLog,
|
|
17
|
+
PublishLogRecord,
|
|
18
|
+
)
|
|
19
|
+
from .models.publish_log import Published
|
|
11
20
|
|
|
12
21
|
|
|
13
22
|
@admin.register(LearningPackage)
|
|
@@ -171,3 +180,69 @@ class PublishedAdmin(ReadOnlyModelAdmin):
|
|
|
171
180
|
|
|
172
181
|
def message(self, published_obj):
|
|
173
182
|
return published_obj.publish_log_record.publish_log.message
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DraftChangeLogRecordTabularInline(admin.TabularInline):
|
|
186
|
+
"""
|
|
187
|
+
Tabular inline for a single Draft change.
|
|
188
|
+
"""
|
|
189
|
+
model = DraftChangeLogRecord
|
|
190
|
+
|
|
191
|
+
fields = (
|
|
192
|
+
"entity",
|
|
193
|
+
"title",
|
|
194
|
+
"old_version_num",
|
|
195
|
+
"new_version_num",
|
|
196
|
+
)
|
|
197
|
+
readonly_fields = fields
|
|
198
|
+
|
|
199
|
+
def get_queryset(self, request):
|
|
200
|
+
queryset = super().get_queryset(request)
|
|
201
|
+
return queryset.select_related("entity", "old_version", "new_version") \
|
|
202
|
+
.order_by("entity__key")
|
|
203
|
+
|
|
204
|
+
def old_version_num(self, draft_change: DraftChangeLogRecord):
|
|
205
|
+
if draft_change.old_version is None:
|
|
206
|
+
return "-"
|
|
207
|
+
return draft_change.old_version.version_num
|
|
208
|
+
|
|
209
|
+
def new_version_num(self, draft_change: DraftChangeLogRecord):
|
|
210
|
+
if draft_change.new_version is None:
|
|
211
|
+
return "-"
|
|
212
|
+
return draft_change.new_version.version_num
|
|
213
|
+
|
|
214
|
+
def title(self, draft_change: DraftChangeLogRecord):
|
|
215
|
+
"""
|
|
216
|
+
Get the title to display for the DraftChange
|
|
217
|
+
"""
|
|
218
|
+
if draft_change.new_version:
|
|
219
|
+
return draft_change.new_version.title
|
|
220
|
+
if draft_change.old_version:
|
|
221
|
+
return draft_change.old_version.title
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@admin.register(DraftChangeLog)
|
|
226
|
+
class DraftChangeSetAdmin(ReadOnlyModelAdmin):
|
|
227
|
+
"""
|
|
228
|
+
Read-only admin to view Draft changes (via inline tables)
|
|
229
|
+
"""
|
|
230
|
+
inlines = [DraftChangeLogRecordTabularInline]
|
|
231
|
+
fields = (
|
|
232
|
+
"uuid",
|
|
233
|
+
"learning_package",
|
|
234
|
+
"num_changes",
|
|
235
|
+
"changed_at",
|
|
236
|
+
"changed_by",
|
|
237
|
+
)
|
|
238
|
+
readonly_fields = fields
|
|
239
|
+
list_display = fields
|
|
240
|
+
list_filter = ["learning_package"]
|
|
241
|
+
|
|
242
|
+
def num_changes(self, draft_change_set):
|
|
243
|
+
return draft_change_set.num_changes
|
|
244
|
+
|
|
245
|
+
def get_queryset(self, request):
|
|
246
|
+
queryset = super().get_queryset(request)
|
|
247
|
+
return queryset.select_related("learning_package", "changed_by") \
|
|
248
|
+
.annotate(num_changes=Count("records"))
|
|
@@ -6,19 +6,24 @@ are stored in this app.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from contextlib import nullcontext
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from datetime import datetime, timezone
|
|
11
12
|
from enum import Enum
|
|
12
|
-
from typing import TypeVar
|
|
13
|
+
from typing import ContextManager, TypeVar
|
|
13
14
|
|
|
14
15
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
15
16
|
from django.db.models import F, Q, QuerySet
|
|
16
17
|
from django.db.transaction import atomic
|
|
17
18
|
|
|
19
|
+
from .contextmanagers import DraftChangeLogContext
|
|
18
20
|
from .models import (
|
|
19
21
|
Container,
|
|
20
22
|
ContainerVersion,
|
|
21
23
|
Draft,
|
|
24
|
+
DraftChangeLog,
|
|
25
|
+
DraftChangeLogRecord,
|
|
26
|
+
DraftSideEffect,
|
|
22
27
|
EntityList,
|
|
23
28
|
EntityListRow,
|
|
24
29
|
LearningPackage,
|
|
@@ -27,10 +32,10 @@ from .models import (
|
|
|
27
32
|
PublishableEntityMixin,
|
|
28
33
|
PublishableEntityVersion,
|
|
29
34
|
PublishableEntityVersionMixin,
|
|
30
|
-
Published,
|
|
31
35
|
PublishLog,
|
|
32
36
|
PublishLogRecord,
|
|
33
37
|
)
|
|
38
|
+
from .models.publish_log import Published
|
|
34
39
|
|
|
35
40
|
# A few of the APIs in this file are generic and can be used for Containers in
|
|
36
41
|
# general, or e.g. Units (subclass of Container) in particular. These type
|
|
@@ -82,6 +87,7 @@ __all__ = [
|
|
|
82
87
|
"contains_unpublished_changes",
|
|
83
88
|
"get_containers_with_entity",
|
|
84
89
|
"get_container_children_count",
|
|
90
|
+
"bulk_draft_changes_for",
|
|
85
91
|
]
|
|
86
92
|
|
|
87
93
|
|
|
@@ -220,10 +226,13 @@ def create_publishable_entity_version(
|
|
|
220
226
|
created=created,
|
|
221
227
|
created_by_id=created_by,
|
|
222
228
|
)
|
|
223
|
-
|
|
224
|
-
entity_id
|
|
225
|
-
|
|
229
|
+
set_draft_version(
|
|
230
|
+
entity_id,
|
|
231
|
+
version.id,
|
|
232
|
+
set_at=created,
|
|
233
|
+
set_by=created_by,
|
|
226
234
|
)
|
|
235
|
+
|
|
227
236
|
return version
|
|
228
237
|
|
|
229
238
|
|
|
@@ -447,25 +456,247 @@ def get_published_version(publishable_entity_id: int, /) -> PublishableEntityVer
|
|
|
447
456
|
|
|
448
457
|
|
|
449
458
|
def set_draft_version(
|
|
450
|
-
|
|
459
|
+
draft_or_id: Draft | int,
|
|
451
460
|
publishable_entity_version_pk: int | None,
|
|
452
461
|
/,
|
|
462
|
+
set_at: datetime | None = None,
|
|
463
|
+
set_by: int | None = None, # User.id
|
|
453
464
|
) -> None:
|
|
454
465
|
"""
|
|
455
466
|
Modify the Draft of a PublishableEntity to be a PublishableEntityVersion.
|
|
456
467
|
|
|
468
|
+
The ``draft`` argument can be either a Draft model object, or the primary
|
|
469
|
+
key of a Draft/PublishableEntity (Draft is defined so these will be the same
|
|
470
|
+
value).
|
|
471
|
+
|
|
457
472
|
This would most commonly be used to set the Draft to point to a newly
|
|
458
473
|
created PublishableEntityVersion that was created in Studio (because someone
|
|
459
474
|
edited some content). Setting a Draft's version to None is like deleting it
|
|
460
475
|
from Studio's editing point of view (see ``soft_delete_draft`` for more
|
|
461
476
|
details).
|
|
477
|
+
|
|
478
|
+
Calling this function attaches a new DraftChangeLogRecordand attaches it to a
|
|
479
|
+
DraftChangeLog.
|
|
480
|
+
|
|
481
|
+
This function will create DraftSideEffect entries and properly add any
|
|
482
|
+
containers that may have been affected by this draft update, UNLESS it is
|
|
483
|
+
called from within a bulk_draft_changes_for block. If it is called from
|
|
484
|
+
inside a bulk_draft_changes_for block, it will not add side-effects for
|
|
485
|
+
containers, as bulk_draft_changes_for will automatically do that when the
|
|
486
|
+
block exits.
|
|
487
|
+
"""
|
|
488
|
+
if set_at is None:
|
|
489
|
+
set_at = datetime.now(tz=timezone.utc)
|
|
490
|
+
|
|
491
|
+
with atomic(savepoint=False):
|
|
492
|
+
if isinstance(draft_or_id, Draft):
|
|
493
|
+
draft = draft_or_id
|
|
494
|
+
elif isinstance(draft_or_id, int):
|
|
495
|
+
draft, _created = Draft.objects.select_related("entity") \
|
|
496
|
+
.get_or_create(entity_id=draft_or_id)
|
|
497
|
+
else:
|
|
498
|
+
class_name = draft_or_id.__class__.__name__
|
|
499
|
+
raise TypeError(
|
|
500
|
+
f"draft_or_id must be a Draft or int, not ({class_name})"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# If the Draft is already pointing at this version, there's nothing to do.
|
|
504
|
+
old_version_id = draft.version_id
|
|
505
|
+
if old_version_id == publishable_entity_version_pk:
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# The actual update of the Draft model is here. Everything after this
|
|
509
|
+
# block is bookkeeping in our DraftChangeLog.
|
|
510
|
+
draft.version_id = publishable_entity_version_pk
|
|
511
|
+
draft.save()
|
|
512
|
+
|
|
513
|
+
# Check to see if we're inside a context manager for an active
|
|
514
|
+
# DraftChangeLog (i.e. what happens if the caller is using the public
|
|
515
|
+
# bulk_draft_changes_for() API call), or if we have to make our own.
|
|
516
|
+
learning_package_id = draft.entity.learning_package_id
|
|
517
|
+
active_change_log = DraftChangeLogContext.get_active_draft_change_log(
|
|
518
|
+
learning_package_id
|
|
519
|
+
)
|
|
520
|
+
if active_change_log:
|
|
521
|
+
change = _add_to_existing_draft_change_log(
|
|
522
|
+
active_change_log,
|
|
523
|
+
draft.entity_id,
|
|
524
|
+
old_version_id=old_version_id,
|
|
525
|
+
new_version_id=publishable_entity_version_pk,
|
|
526
|
+
)
|
|
527
|
+
# We explicitly *don't* create container side effects here because
|
|
528
|
+
# there may be many changes in this DraftChangeLog, some of which
|
|
529
|
+
# haven't been made yet. It wouldn't make sense to create a side
|
|
530
|
+
# effect that says, "this Unit changed because this Component in it
|
|
531
|
+
# changed" if we were changing that same Unit later on in the same
|
|
532
|
+
# DraftChangeLog, because that new Unit version might not even
|
|
533
|
+
# include the child Component. So we'll let DraftChangeLogContext
|
|
534
|
+
# do that work when it exits its context.
|
|
535
|
+
else:
|
|
536
|
+
# This means there is no active DraftChangeLog, so we create our own
|
|
537
|
+
# and add our DraftChangeLogRecord to it. This has the minor
|
|
538
|
+
# optimization that we don't have to check for an existing
|
|
539
|
+
# DraftChangeLogRecord, because we know it can't exist yet.
|
|
540
|
+
change_log = DraftChangeLog.objects.create(
|
|
541
|
+
learning_package_id=learning_package_id,
|
|
542
|
+
changed_at=set_at,
|
|
543
|
+
changed_by_id=set_by,
|
|
544
|
+
)
|
|
545
|
+
change = DraftChangeLogRecord.objects.create(
|
|
546
|
+
draft_change_log=change_log,
|
|
547
|
+
entity_id=draft.entity_id,
|
|
548
|
+
old_version_id=old_version_id,
|
|
549
|
+
new_version_id=publishable_entity_version_pk,
|
|
550
|
+
)
|
|
551
|
+
_create_container_side_effects_for_draft_change(change)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _add_to_existing_draft_change_log(
|
|
555
|
+
active_change_log: DraftChangeLog,
|
|
556
|
+
entity_id: int,
|
|
557
|
+
old_version_id: int | None,
|
|
558
|
+
new_version_id: int | None,
|
|
559
|
+
) -> DraftChangeLogRecord:
|
|
560
|
+
"""
|
|
561
|
+
Create or update a DraftChangeLogRecord for the active_change_log passed in.
|
|
562
|
+
|
|
563
|
+
The an active_change_log may have many DraftChangeLogRecords already
|
|
564
|
+
associated with it. A DraftChangeLog can only have one DraftChangeLogRecord
|
|
565
|
+
per PublishableEntity, e.g. the same Component can't go from v1 to v2 and v2
|
|
566
|
+
to v3 in the same DraftChangeLog. The DraftChangeLogRecord is meant to
|
|
567
|
+
capture the before and after states of the Draft version for that entity,
|
|
568
|
+
so we always keep the first value for old_version, while updating to the
|
|
569
|
+
most recent value for new_version.
|
|
570
|
+
|
|
571
|
+
So for example, if we called this function with the same active_change_log
|
|
572
|
+
and the same entity_id but with versions: (None, v1), (v1, v2), (v2, v3);
|
|
573
|
+
we would collapse them into one DraftChangeLogrecord with old_version = None
|
|
574
|
+
and new_version = v3.
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
# Check to see if this PublishableEntity has already been changed in
|
|
578
|
+
# this DraftChangeLog. If so, we update that record instead of creating
|
|
579
|
+
# a new one.
|
|
580
|
+
change = DraftChangeLogRecord.objects.get(
|
|
581
|
+
draft_change_log=active_change_log,
|
|
582
|
+
entity_id=entity_id,
|
|
583
|
+
)
|
|
584
|
+
if change.old_version_id == new_version_id:
|
|
585
|
+
# Special case: This change undoes the previous change(s). The value
|
|
586
|
+
# in change.old_version_id represents the Draft version before the
|
|
587
|
+
# DraftChangeLog was started, regardless of how many times we've
|
|
588
|
+
# changed it since we entered the bulk_draft_changes_for() context.
|
|
589
|
+
# If we get here in the code, it means that we're now setting the
|
|
590
|
+
# Draft version of this entity to be exactly what it was at the
|
|
591
|
+
# start, and we should remove it entirely from the DraftChangeLog.
|
|
592
|
+
#
|
|
593
|
+
# It's important that we remove these cases, because we use the
|
|
594
|
+
# old_version == new_version convention to record entities that have
|
|
595
|
+
# changed purely due to side-effects.
|
|
596
|
+
change.delete()
|
|
597
|
+
else:
|
|
598
|
+
# Normal case: We update the new_version, but leave the old_version
|
|
599
|
+
# as is. The old_version represents what the Draft was pointing to
|
|
600
|
+
# when the bulk_draft_changes_for() context started, so it persists
|
|
601
|
+
# if we change the same entity multiple times in the DraftChangeLog.
|
|
602
|
+
change.new_version_id = new_version_id
|
|
603
|
+
change.save()
|
|
604
|
+
except DraftChangeLogRecord.DoesNotExist:
|
|
605
|
+
# If we're here, this is the first DraftChangeLogRecord we're making for
|
|
606
|
+
# this PublishableEntity in the active DraftChangeLog.
|
|
607
|
+
change = DraftChangeLogRecord.objects.create(
|
|
608
|
+
draft_change_log=active_change_log,
|
|
609
|
+
entity_id=entity_id,
|
|
610
|
+
old_version_id=old_version_id,
|
|
611
|
+
new_version_id=new_version_id,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
return change
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _create_container_side_effects_for_draft_change_log(change_log: DraftChangeLog):
|
|
462
618
|
"""
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
619
|
+
Iterate through the whole DraftChangeLog and process side-effects.
|
|
620
|
+
"""
|
|
621
|
+
processed_entity_ids: set[int] = set()
|
|
622
|
+
for change in change_log.records.all():
|
|
623
|
+
_create_container_side_effects_for_draft_change(
|
|
624
|
+
change,
|
|
625
|
+
processed_entity_ids=processed_entity_ids,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _create_container_side_effects_for_draft_change(
|
|
630
|
+
original_change: DraftChangeLogRecord,
|
|
631
|
+
processed_entity_ids: set | None = None
|
|
632
|
+
):
|
|
633
|
+
"""
|
|
634
|
+
Given a draft change, add side effects for all affected containers.
|
|
635
|
+
|
|
636
|
+
This should only be run after the DraftChangeLogRecord has been otherwise
|
|
637
|
+
fully written out. We want to avoid the scenario where we create a
|
|
638
|
+
side-effect that a Component change affects a Unit if the Unit version is
|
|
639
|
+
also changed (maybe even deleted) in the same DraftChangeLog.
|
|
640
|
+
|
|
641
|
+
The `processed_entity_ids` set holds the entity IDs that we've already
|
|
642
|
+
calculated side-effects for. This is to save us from recalculating side-
|
|
643
|
+
effects for the same ancestor relationships over and over again. So if we're
|
|
644
|
+
calling this function in a loop for all the Components in a Unit, we won't
|
|
645
|
+
be recalculating the Unit's side-effect on its Subsection, and its
|
|
646
|
+
Subsection's side-effect on its Section.
|
|
647
|
+
|
|
648
|
+
TODO: This could get very expensive with the get_containers_with_entity
|
|
649
|
+
calls. We should measure the impact of this.
|
|
650
|
+
"""
|
|
651
|
+
if processed_entity_ids is None:
|
|
652
|
+
# An optimization, but also a guard against infinite side-effect loops.
|
|
653
|
+
processed_entity_ids = set()
|
|
654
|
+
|
|
655
|
+
changes_and_containers = [
|
|
656
|
+
(original_change, container)
|
|
657
|
+
for container
|
|
658
|
+
in get_containers_with_entity(original_change.entity_id, ignore_pinned=True)
|
|
659
|
+
]
|
|
660
|
+
while changes_and_containers:
|
|
661
|
+
change, container = changes_and_containers.pop()
|
|
662
|
+
|
|
663
|
+
# If the container is not already in the DraftChangeLog, we need to
|
|
664
|
+
# add it. Since it's being caused as a DraftSideEffect, we're going
|
|
665
|
+
# add it with the old_version == new_version convention.
|
|
666
|
+
container_draft_version_pk = container.versioning.draft.pk
|
|
667
|
+
container_change, _created = DraftChangeLogRecord.objects.get_or_create(
|
|
668
|
+
draft_change_log=change.draft_change_log,
|
|
669
|
+
entity_id=container.pk,
|
|
670
|
+
defaults={
|
|
671
|
+
'old_version_id': container_draft_version_pk,
|
|
672
|
+
'new_version_id': container_draft_version_pk
|
|
673
|
+
}
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Mark that change in the current loop has the side effect of changing
|
|
677
|
+
# the parent container. We'll do this regardless of whether the
|
|
678
|
+
# container version itself also changed. If a Unit has a Component and
|
|
679
|
+
# both the Unit and Component have their versions incremented, then the
|
|
680
|
+
# Unit has changed in both ways (the Unit's internal metadata as well as
|
|
681
|
+
# the new version of the child component).
|
|
682
|
+
DraftSideEffect.objects.get_or_create(cause=change, effect=container_change)
|
|
683
|
+
processed_entity_ids.add(change.entity_id)
|
|
684
|
+
|
|
685
|
+
# Now we find the next layer up of containers. So if the originally
|
|
686
|
+
# passed in publishable_entity_id was for a Component, then the
|
|
687
|
+
# ``container`` we've been creating the side effect for in this loop
|
|
688
|
+
# is the Unit, and ``parents_of_container`` would be any Sequences
|
|
689
|
+
# that contain the Unit.
|
|
690
|
+
parents_of_container = get_containers_with_entity(container.pk, ignore_pinned=True)
|
|
691
|
+
|
|
692
|
+
changes_and_containers.extend(
|
|
693
|
+
(container_change, container_parent)
|
|
694
|
+
for container_parent in parents_of_container
|
|
695
|
+
if container_parent.pk not in processed_entity_ids
|
|
696
|
+
)
|
|
466
697
|
|
|
467
698
|
|
|
468
|
-
def soft_delete_draft(publishable_entity_id: int,
|
|
699
|
+
def soft_delete_draft(publishable_entity_id: int, /, deleted_by: int | None = None) -> None:
|
|
469
700
|
"""
|
|
470
701
|
Sets the Draft version to None.
|
|
471
702
|
|
|
@@ -475,10 +706,15 @@ def soft_delete_draft(publishable_entity_id: int, /) -> None:
|
|
|
475
706
|
of pointing the Draft back to the most recent ``PublishableEntityVersion``
|
|
476
707
|
for a given ``PublishableEntity``.
|
|
477
708
|
"""
|
|
478
|
-
return set_draft_version(publishable_entity_id, None)
|
|
709
|
+
return set_draft_version(publishable_entity_id, None, set_by=deleted_by)
|
|
479
710
|
|
|
480
711
|
|
|
481
|
-
def reset_drafts_to_published(
|
|
712
|
+
def reset_drafts_to_published(
|
|
713
|
+
learning_package_id: int,
|
|
714
|
+
/,
|
|
715
|
+
reset_at: datetime | None = None,
|
|
716
|
+
reset_by: int | None = None, # User.id
|
|
717
|
+
) -> None:
|
|
482
718
|
"""
|
|
483
719
|
Reset all Drafts to point to the most recently Published versions.
|
|
484
720
|
|
|
@@ -502,27 +738,55 @@ def reset_drafts_to_published(learning_package_id: int, /) -> None:
|
|
|
502
738
|
it's important that the code creating the "next" version_num looks at the
|
|
503
739
|
latest version created for a PublishableEntity (its ``latest`` attribute),
|
|
504
740
|
rather than basing it off of the version that Draft points to.
|
|
505
|
-
|
|
506
|
-
Also, there is no current immutable record for when a reset happens. It's
|
|
507
|
-
not like a publish that leaves an entry in the ``PublishLog``.
|
|
508
741
|
"""
|
|
742
|
+
if reset_at is None:
|
|
743
|
+
reset_at = datetime.now(tz=timezone.utc)
|
|
744
|
+
|
|
509
745
|
# These are all the drafts that are different from the published versions.
|
|
510
746
|
draft_qset = Draft.objects \
|
|
511
747
|
.select_related("entity__published") \
|
|
512
748
|
.filter(entity__learning_package_id=learning_package_id) \
|
|
513
|
-
.exclude(entity__published__version_id=F("version_id"))
|
|
749
|
+
.exclude(entity__published__version_id=F("version_id")) \
|
|
750
|
+
.exclude(
|
|
751
|
+
# NULL != NULL in SQL, so we want to exclude entries
|
|
752
|
+
# where both the published version and draft version
|
|
753
|
+
# are None. This edge case happens when we create
|
|
754
|
+
# something and then delete it without publishing, and
|
|
755
|
+
# then reset Drafts to their published state.
|
|
756
|
+
Q(entity__published__version__isnull=True) &
|
|
757
|
+
Q(version__isnull=True)
|
|
758
|
+
)
|
|
759
|
+
# If there's nothing to reset because there are no changes from the
|
|
760
|
+
# published version, just return early rather than making an empty
|
|
761
|
+
# DraftChangeLog.
|
|
762
|
+
if not draft_qset:
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
active_change_log = DraftChangeLogContext.get_active_draft_change_log(learning_package_id)
|
|
766
|
+
|
|
767
|
+
# If there's an active DraftChangeLog, we're already in a transaction, so
|
|
768
|
+
# there's no need to open a new one.
|
|
769
|
+
tx_context: ContextManager
|
|
770
|
+
if active_change_log:
|
|
771
|
+
tx_context = nullcontext()
|
|
772
|
+
else:
|
|
773
|
+
tx_context = bulk_draft_changes_for(
|
|
774
|
+
learning_package_id, changed_at=reset_at, changed_by=reset_by
|
|
775
|
+
)
|
|
514
776
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
777
|
+
with tx_context:
|
|
778
|
+
# Note: We can't do an .update with a F() on a joined field in the ORM,
|
|
779
|
+
# so we have to loop through the drafts individually to reset them
|
|
780
|
+
# anyhow. We can rework this into a bulk update or custom SQL if it
|
|
781
|
+
# becomes a performance issue, as long as we also port over the
|
|
782
|
+
# bookkeeping code in set_draft_version.
|
|
520
783
|
for draft in draft_qset:
|
|
521
784
|
if hasattr(draft.entity, 'published'):
|
|
522
|
-
|
|
785
|
+
published_version_id = draft.entity.published.version_id
|
|
523
786
|
else:
|
|
524
|
-
|
|
525
|
-
|
|
787
|
+
published_version_id = None
|
|
788
|
+
|
|
789
|
+
set_draft_version(draft, published_version_id)
|
|
526
790
|
|
|
527
791
|
|
|
528
792
|
def register_content_models(
|
|
@@ -1103,6 +1367,7 @@ def get_containers_with_entity(
|
|
|
1103
1367
|
ignore_pinned: if true, ignore any pinned references to the entity.
|
|
1104
1368
|
"""
|
|
1105
1369
|
if ignore_pinned:
|
|
1370
|
+
# TODO: Do we need to run distinct() on this? Will fix in https://github.com/openedx/openedx-learning/issues/303
|
|
1106
1371
|
qs = Container.objects.filter(
|
|
1107
1372
|
# Note: these two conditions must be in the same filter() call, or the query won't be correct.
|
|
1108
1373
|
publishable_entity__draft__version__containerversion__entity_list__entitylistrow__entity_id=publishable_entity_pk, # pylint: disable=line-too-long # noqa: E501
|
|
@@ -1145,3 +1410,47 @@ def get_container_children_count(
|
|
|
1145
1410
|
else:
|
|
1146
1411
|
filter_deleted = {"entity__draft__version__isnull": False}
|
|
1147
1412
|
return container_version.entity_list.entitylistrow_set.filter(**filter_deleted).count()
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
def bulk_draft_changes_for(
|
|
1416
|
+
learning_package_id: int,
|
|
1417
|
+
changed_by: int | None = None,
|
|
1418
|
+
changed_at: datetime | None = None
|
|
1419
|
+
) -> DraftChangeLogContext:
|
|
1420
|
+
"""
|
|
1421
|
+
Context manager to do a single batch of Draft changes in.
|
|
1422
|
+
|
|
1423
|
+
Each publishable entity that is edited in this context will be tied to a
|
|
1424
|
+
single DraftChangeLogRecord, representing the cumulative changes made to
|
|
1425
|
+
that entity. Upon closing of the context, side effects of these changes will
|
|
1426
|
+
be calcuated, which may result in more DraftChangeLogRecords being created
|
|
1427
|
+
or updated. The resulting DraftChangeLogRecords and DraftChangeSideEffects
|
|
1428
|
+
will be tied together into a single DraftChangeLog, representing the
|
|
1429
|
+
collective changes to the learning package that happened in this context.
|
|
1430
|
+
All changes will be committed in a single atomic transaction.
|
|
1431
|
+
|
|
1432
|
+
Example::
|
|
1433
|
+
|
|
1434
|
+
with bulk_draft_changes_for(learning_package.id):
|
|
1435
|
+
for section in course:
|
|
1436
|
+
update_section_drafts(learning_package.id, section)
|
|
1437
|
+
|
|
1438
|
+
If you make a change to an entity *without* using this context manager, then
|
|
1439
|
+
the individual change (and its side effects) will be automatically wrapped
|
|
1440
|
+
in a one-off change context. For example, this::
|
|
1441
|
+
|
|
1442
|
+
update_one_component(component.learning_package, component)
|
|
1443
|
+
|
|
1444
|
+
is identical to this::
|
|
1445
|
+
|
|
1446
|
+
with bulk_draft_changes_for(component.learning_package.id):
|
|
1447
|
+
update_one_component(component.learning_package.id, component)
|
|
1448
|
+
"""
|
|
1449
|
+
return DraftChangeLogContext(
|
|
1450
|
+
learning_package_id,
|
|
1451
|
+
changed_at=changed_at,
|
|
1452
|
+
changed_by=changed_by,
|
|
1453
|
+
exit_callbacks=[
|
|
1454
|
+
_create_container_side_effects_for_draft_change_log,
|
|
1455
|
+
]
|
|
1456
|
+
)
|