|
23 | 23 | Draft, |
24 | 24 | DraftChangeLogRecord, |
25 | 25 | DraftChangeLog, |
| 26 | + DraftSideEffect, |
26 | 27 | EntityList, |
27 | 28 | EntityListRow, |
28 | 29 | LearningPackage, |
@@ -474,57 +475,148 @@ def set_draft_version( |
474 | 475 | # DraftChangeLog (i.e. what happens if the caller is using the public |
475 | 476 | # bulk_draft_changes_for() API call), or if we have to make our own. |
476 | 477 | active_change_log = DraftChangeLogContext.get_active_draft_change_log(learning_package_id) |
| 478 | + change_log = active_change_log or DraftChangeLog.objects.create( |
| 479 | + learning_package_id=learning_package_id, |
| 480 | + changed_at=set_at, |
| 481 | + changed_by_id=set_by, |
| 482 | + ) |
477 | 483 |
|
478 | 484 | if active_change_log: |
479 | | - # If we get here, it means there is an active DraftChangeLog that may |
480 | | - # have many DraftChanges associated with it. A DraftChangeLog can only |
481 | | - # have one DraftChange per PublishableEntity, e.g. the same Component |
482 | | - # can't go from v1 to v3 and v1 to v4 in the same DraftChangeLog. |
483 | | - try: |
484 | | - # If there was already a DraftChange for this PublishableEntity, |
485 | | - # we update the new_version_id. We keep the old_version_id value |
486 | | - # though, because that represents what it was before this |
487 | | - # DraftChangeLog, and we don't want to lose that information. |
488 | | - change = DraftChangeLogRecord.objects.get( |
489 | | - draft_change_log=active_change_log, |
490 | | - entity_id=publishable_entity_id, |
491 | | - ) |
492 | | - change.new_version_id = publishable_entity_version_pk |
493 | | - change.save() |
494 | | - except DraftChangeLogRecord.DoesNotExist: |
495 | | - # If we're here, this is the first DraftChange we're making for |
496 | | - # this PublishableEntity in the active DraftChangeLog. |
497 | | - change = DraftChangeLogRecord.objects.create( |
498 | | - draft_change_log=active_change_log, |
499 | | - entity_id=publishable_entity_id, |
500 | | - old_version_id=old_version_id, |
501 | | - new_version_id=publishable_entity_version_pk, |
502 | | - ) |
| 485 | + change = _add_to_existing_draft_change_log( |
| 486 | + active_change_log, |
| 487 | + publishable_entity_id, |
| 488 | + old_version_id=old_version_id, |
| 489 | + new_version_id=publishable_entity_version_pk, |
| 490 | + ) |
| 491 | + # We explicitly *don't* create container side effects here because |
| 492 | + # there may be many changes in this DraftChangeLog, some of which |
| 493 | + # haven't been made yet. It wouldn't make sense to create a side |
| 494 | + # effect that says, "this Unit changed because this Component in it |
| 495 | + # changed" if we were changing that same Unit later on in the same |
| 496 | + # DraftChangeLog, because that new Unit version might not even |
| 497 | + # include the child Component. So we'll do that work when the |
| 498 | + # DraftChangeLogContext context manager closes. |
503 | 499 | else: |
504 | 500 | # This means there is no active DraftChangeLog, so we create our own |
505 | 501 | # add add our DraftChange to it. This has the minor optimization |
506 | 502 | # that we don't have to check for an existing DraftChange, because |
507 | 503 | # we're creating the whole DraftChangeLog right here. |
508 | | - new_change_log = DraftChangeLog.objects.create( |
509 | | - learning_package_id=learning_package_id, |
510 | | - changed_at=set_at, |
511 | | - changed_by_id=set_by, |
512 | | - ) |
513 | 504 | change = DraftChangeLogRecord.objects.create( |
514 | | - draft_change_log=new_change_log, |
| 505 | + draft_change_log=change_log, |
515 | 506 | entity_id=publishable_entity_id, |
516 | 507 | old_version_id=old_version_id, |
517 | 508 | new_version_id=publishable_entity_version_pk, |
518 | | - ) |
| 509 | + ) |
| 510 | + _create_container_side_effects_for_draft_change(change) |
| 511 | + |
| 512 | + |
| 513 | +def _add_to_existing_draft_change_log( |
| 514 | + active_change_log: DraftChangeLog, |
| 515 | + entity_id: int, |
| 516 | + old_version_id: int | None, |
| 517 | + new_version_id: int | None, |
| 518 | +) -> DraftChangeLogRecord: |
| 519 | + # If we get here, it means there is an active DraftChangeLog that may |
| 520 | + # have many DraftChanges associated with it. A DraftChangeLog can only |
| 521 | + # have one DraftChange per PublishableEntity, e.g. the same Component |
| 522 | + # can't go from v1 to v3 and v1 to v4 in the same DraftChangeLog. |
| 523 | + try: |
| 524 | + # If there was already a DraftChange for this PublishableEntity, |
| 525 | + # we update the new_version_id. We keep the old_version_id value |
| 526 | + # though, because that represents what it was before this |
| 527 | + # DraftChangeLog, and we don't want to lose that information. |
| 528 | + change = DraftChangeLogRecord.objects.get( |
| 529 | + draft_change_log=active_change_log, |
| 530 | + entity_id=entity_id, |
| 531 | + ) |
| 532 | + change.new_version_id = new_version_id |
| 533 | + change.save() |
| 534 | + except DraftChangeLogRecord.DoesNotExist: |
| 535 | + # If we're here, this is the first DraftChange we're making for |
| 536 | + # this PublishableEntity in the active DraftChangeLog. |
| 537 | + change = DraftChangeLogRecord.objects.create( |
| 538 | + draft_change_log=active_change_log, |
| 539 | + entity_id=entity_id, |
| 540 | + old_version_id=old_version_id, |
| 541 | + new_version_id=new_version_id, |
| 542 | + ) |
519 | 543 |
|
520 | | - # One way or another, we've created our DraftChange at this point. Now |
521 | | - # to see if we need to add any parent Draft containers that this change |
522 | | - # may have affected (i.e. all unpinned references from Draft containers |
523 | | - # to the entity that we just changed the Draft entry for)... |
524 | | - containers = get_containers_with_entity(publishable_entity_id, ignore_pinned=True) |
| 544 | + return change |
525 | 545 |
|
526 | 546 |
|
| 547 | +def _create_container_side_effects_for_draft_change_log(change_log: DraftChangeLogRecord): |
| 548 | + seen_container_pks = set() |
| 549 | + for change in change_log.changes.all(): |
| 550 | + _create_container_side_effects_for_draft_change( |
| 551 | + change, |
| 552 | + already_processed_containers_pks=seen_container_pks, |
| 553 | + ) |
| 554 | + |
| 555 | + |
| 556 | +def _create_container_side_effects_for_draft_change( |
| 557 | + original_change: DraftChangeLogRecord, |
| 558 | + already_processed_containers_pks=None |
| 559 | +): |
| 560 | + """ |
| 561 | + Given a draft change, add side effects for all affected containers. |
527 | 562 |
|
| 563 | + This should only be run after the DraftChangeLogRecord has been otherwise |
| 564 | + fully written out. We want to avoid the scenario where we create a |
| 565 | + side-effect that a Component change affects a Unit if the Unit version is |
| 566 | + also changed in the same DraftChangeLog. |
| 567 | +
|
| 568 | + TODO: This could get very expensive with the get_containers_with_entity |
| 569 | + calls. We should measure the impact of this. |
| 570 | + """ |
| 571 | + if already_processed_containers_pks is not None: |
| 572 | + seen_container_pks = already_processed_containers_pks |
| 573 | + else: |
| 574 | + seen_container_pks = set() |
| 575 | + |
| 576 | + changes_and_containers = [ |
| 577 | + (original_change, container) |
| 578 | + for container |
| 579 | + in get_containers_with_entity(original_change.entity_id, ignore_pinned=True) |
| 580 | + ] |
| 581 | + while changes_and_containers: |
| 582 | + change, container = changes_and_containers.pop() |
| 583 | + # If we've already seen this container, no need to handle it again. |
| 584 | + # This also guards against infinite cycle chains of containers. |
| 585 | + if container.pk in seen_container_pks: |
| 586 | + continue |
| 587 | + seen_container_pks.add(container.pk) |
| 588 | + |
| 589 | + # If the container is not already in the DraftChangeLog, we need to |
| 590 | + # add it. Since it's being caused as a DraftSideEffect, we're going |
| 591 | + # add it with the old_version == new_version convention. |
| 592 | + container_draft_version_pk = container.versioning.draft.pk |
| 593 | + container_change, _created = DraftChangeLogRecord.objects.get_or_create( |
| 594 | + draft_change_log=change.draft_change_log, |
| 595 | + entity_id=container.pk, |
| 596 | + old_version_id=container_draft_version_pk, |
| 597 | + defaults={ |
| 598 | + 'new_version_id': container_draft_version_pk |
| 599 | + } |
| 600 | + ) |
| 601 | + |
| 602 | + # Mark that change in the current loop has the side effect of changing |
| 603 | + # the parent. We'll do this regardless of whether the container version |
| 604 | + # itself also changed. If a Unit has a Component and both the Unit and |
| 605 | + # Component have their versions incremented, then the Unit has changed |
| 606 | + # in both ways (the Unit's internal metadata as well as the new version |
| 607 | + # of the child component). |
| 608 | + DraftSideEffect.objects.get_or_create(cause=change, effect=container_change) |
| 609 | + |
| 610 | + # Now we find the next layer up of containers. So if the originally |
| 611 | + # passed in publishable_entity_id was for a Component, then the |
| 612 | + # ``container`` we've been creating the side effect for in this loop |
| 613 | + # is the Unit, and ``parents_of_container`` would be any Sequences |
| 614 | + # that contain the Unit. |
| 615 | + parents_of_container = get_containers_with_entity(container.pk, ignore_pinned=True) |
| 616 | + changes_and_containers.extend( |
| 617 | + (container_change, container_parent) |
| 618 | + for container_parent in parents_of_container |
| 619 | + ) |
528 | 620 |
|
529 | 621 | def soft_delete_draft(publishable_entity_id: int, /, deleted_by: int | None = None) -> None: |
530 | 622 | """ |
@@ -1107,10 +1199,17 @@ def get_containers_with_entity( |
1107 | 1199 | return qs |
1108 | 1200 |
|
1109 | 1201 |
|
1110 | | -def bulk_draft_changes_for(learning_package_id: int) -> DraftChangeLog: |
| 1202 | +def bulk_draft_changes_for(learning_package_id: int, changed_by=None, changed_at=None) -> DraftChangeLog: |
1111 | 1203 | """ |
1112 | 1204 | Context manager to do a single batch of Draft changes in. |
1113 | 1205 |
|
1114 | 1206 | TODO: better description here with example usage. |
1115 | 1207 | """ |
1116 | | - return DraftChangeLogContext(learning_package_id) |
| 1208 | + return DraftChangeLogContext( |
| 1209 | + learning_package_id, |
| 1210 | + changed_by=changed_by, |
| 1211 | + changed_at=changed_at, |
| 1212 | + exit_callbacks=[ |
| 1213 | + _create_container_side_effects_for_draft_change_log, |
| 1214 | + ] |
| 1215 | + ) |
0 commit comments