|
11 | 11 | from django.db import models
|
12 | 12 | from django.db.models import ManyToManyField
|
13 | 13 | from django.db.models.fields.proxy import OrderWrt
|
| 14 | +from django.db.models.fields.related import ForeignKey |
| 15 | +from django.db.models.fields.related_descriptors import ( |
| 16 | + ForwardManyToOneDescriptor, |
| 17 | + ReverseManyToOneDescriptor, |
| 18 | + create_reverse_many_to_one_manager, |
| 19 | +) |
| 20 | +from django.db.models.query import QuerySet |
14 | 21 | from django.forms.models import model_to_dict
|
15 | 22 | from django.urls import reverse
|
16 | 23 | from django.utils import timezone
|
17 | 24 | from django.utils.encoding import smart_str
|
| 25 | +from django.utils.functional import cached_property |
18 | 26 | from django.utils.text import format_lazy
|
19 | 27 | from django.utils.translation import gettext_lazy as _
|
20 | 28 |
|
21 | 29 | from simple_history import utils
|
22 | 30 |
|
23 | 31 | from . import exceptions
|
24 |
| -from .manager import HistoryDescriptor |
| 32 | +from .manager import SIMPLE_HISTORY_REVERSE_ATTR_NAME, HistoryDescriptor |
25 | 33 | from .signals import post_create_historical_record, pre_create_historical_record
|
26 | 34 | from .utils import get_change_reason_from_object
|
27 | 35 |
|
@@ -422,7 +430,10 @@ def get_instance(self):
|
422 | 430 | pass
|
423 | 431 | else:
|
424 | 432 | attrs.update(values)
|
425 |
| - return model(**attrs) |
| 433 | + result = model(**attrs) |
| 434 | + # this is the only way external code could know an instance is historical |
| 435 | + setattr(result, SIMPLE_HISTORY_REVERSE_ATTR_NAME, self) |
| 436 | + return result |
426 | 437 |
|
427 | 438 | def get_next_record(self):
|
428 | 439 | """
|
@@ -632,6 +643,108 @@ def transform_field(field):
|
632 | 643 | field.serialize = True
|
633 | 644 |
|
634 | 645 |
|
| 646 | +class HistoricForwardManyToOneDescriptor(ForwardManyToOneDescriptor): |
| 647 | + """ |
| 648 | + Overrides get_queryset to provide historic query support, should the |
| 649 | + instance be historic (and therefore was generated by a timepoint query) |
| 650 | + and the other side of the relation also uses a history manager. |
| 651 | + """ |
| 652 | + |
| 653 | + def get_queryset(self, **hints) -> QuerySet: |
| 654 | + instance = hints.get("instance") |
| 655 | + if instance: |
| 656 | + history = getattr(instance, SIMPLE_HISTORY_REVERSE_ATTR_NAME, None) |
| 657 | + histmgr = getattr( |
| 658 | + self.field.remote_field.model, |
| 659 | + getattr( |
| 660 | + self.field.remote_field.model._meta, |
| 661 | + "simple_history_manager_attribute", |
| 662 | + "_notthere", |
| 663 | + ), |
| 664 | + None, |
| 665 | + ) |
| 666 | + if history and histmgr: |
| 667 | + return histmgr.as_of(history._as_of) |
| 668 | + return super().get_queryset(**hints) |
| 669 | + |
| 670 | + |
| 671 | +class HistoricReverseManyToOneDescriptor(ReverseManyToOneDescriptor): |
| 672 | + """ |
| 673 | + Overrides get_queryset to provide historic query support, should the |
| 674 | + instance be historic (and therefore was generated by a timepoint query) |
| 675 | + and the other side of the relation also uses a history manager. |
| 676 | + """ |
| 677 | + |
| 678 | + @cached_property |
| 679 | + def related_manager_cls(self): |
| 680 | + related_model = self.rel.related_model |
| 681 | + |
| 682 | + class HistoricRelationModelManager(related_model._default_manager.__class__): |
| 683 | + def get_queryset(self): |
| 684 | + try: |
| 685 | + return self.instance._prefetched_objects_cache[ |
| 686 | + self.field.remote_field.get_cache_name() |
| 687 | + ] |
| 688 | + except (AttributeError, KeyError): |
| 689 | + history = getattr( |
| 690 | + self.instance, SIMPLE_HISTORY_REVERSE_ATTR_NAME, None |
| 691 | + ) |
| 692 | + histmgr = getattr( |
| 693 | + self.model, |
| 694 | + getattr( |
| 695 | + self.model._meta, |
| 696 | + "simple_history_manager_attribute", |
| 697 | + "_notthere", |
| 698 | + ), |
| 699 | + None, |
| 700 | + ) |
| 701 | + if history and histmgr: |
| 702 | + queryset = histmgr.as_of(history._as_of) |
| 703 | + else: |
| 704 | + queryset = super().get_queryset() |
| 705 | + return self._apply_rel_filters(queryset) |
| 706 | + |
| 707 | + return create_reverse_many_to_one_manager( |
| 708 | + HistoricRelationModelManager, self.rel |
| 709 | + ) |
| 710 | + |
| 711 | + |
| 712 | +class HistoricForeignKey(ForeignKey): |
| 713 | + """ |
| 714 | + Allows foreign keys to work properly from a historic instance. |
| 715 | +
|
| 716 | + If you use as_of queries to extract historical instances from |
| 717 | + a model, and you have other models that are related by foreign |
| 718 | + key and also historic, changing them to a HistoricForeignKey |
| 719 | + field type will allow you to naturally cross the relationship |
| 720 | + boundary at the same point in time as the origin instance. |
| 721 | +
|
| 722 | + A historic instance maintains an attribute ("_historic") when |
| 723 | + it is historic, holding the historic record instance and the |
| 724 | + timepoint used to query it ("_as_of"). HistoricForeignKey |
| 725 | + looks for this and uses an as_of query against the related |
| 726 | + object so the relationship is assessed at the same timepoint. |
| 727 | + """ |
| 728 | + |
| 729 | + forward_related_accessor_class = HistoricForwardManyToOneDescriptor |
| 730 | + related_accessor_class = HistoricReverseManyToOneDescriptor |
| 731 | + |
| 732 | + |
| 733 | +def is_historic(instance): |
| 734 | + """ |
| 735 | + Returns True if the instance was acquired with an as_of timepoint. |
| 736 | + """ |
| 737 | + return to_historic(instance) is not None |
| 738 | + |
| 739 | + |
| 740 | +def to_historic(instance): |
| 741 | + """ |
| 742 | + Returns a historic model instance if the instance was acquired with |
| 743 | + an as_of timepoint, or None. |
| 744 | + """ |
| 745 | + return getattr(instance, SIMPLE_HISTORY_REVERSE_ATTR_NAME, None) |
| 746 | + |
| 747 | + |
635 | 748 | class HistoricalObjectDescriptor:
|
636 | 749 | def __init__(self, model, fields_included):
|
637 | 750 | self.model = model
|
|
0 commit comments