Coverage for apis_core/history/models.py: 54%
95 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-19 16:54 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-19 16:54 +0000
1import inspect
2from typing import Any
4import django
5from django.conf import settings
6from django.contrib.contenttypes.models import ContentType
7from django.core.exceptions import AppRegistryNotReady
8from django.db import models
9from django.db.models import Q, UniqueConstraint
10from django.db.models.functions import Lower
11from django.urls import reverse
12from django.utils import timezone
13from simple_history import utils
14from simple_history.models import HistoricalRecords
16from apis_core.generic.abc import GenericModel
19class APISHistoricalRecords(HistoricalRecords, GenericModel):
20 def get_m2m_fields_from_model(self, model):
21 # Change the original simple history function to also return m2m fields
22 m2m_fields = []
23 try:
24 for field in inspect.getmembers(model):
25 if isinstance(
26 field[1],
27 django.db.models.fields.related_descriptors.ManyToManyDescriptor,
28 ):
29 m2m_fields.append(getattr(model, field[0]).field)
30 except AppRegistryNotReady:
31 pass
32 return m2m_fields
34 def get_prev_record(self):
35 """
36 Get the previous history record for the instance. `None` if first.
37 """
38 history = utils.get_history_manager_from_history(self)
39 return (
40 history.filter(history_date__lt=self.history_date)
41 .order_by("history_date")
42 .first()
43 )
46class APISHistoryTableBase(models.Model, GenericModel):
47 version_tag = models.CharField(max_length=255, blank=True, null=True)
49 class Meta:
50 abstract = True
51 constraints = [
52 UniqueConstraint(
53 fields=["id", Lower("version_tag")], name="id_version_tag_unique"
54 ),
55 ]
57 def set_version_tag(self, tag: str):
58 self.version_tag = tag
59 self.save()
61 def get_absolute_url(self):
62 ct = ContentType.objects.get_for_model(self)
63 return reverse("apis_core:generic:detail", args=[ct, self.history_id])
65 def get_diff(self, other_version=None):
66 if self.history_type == "-":
67 return None
68 version = other_version or self.prev_record
69 if version:
70 delta = self.diff_against(version)
71 else:
72 delta = self.diff_against(self.__class__())
73 changes = list(
74 filter(
75 lambda x: (x.new != "" or x.old is not None)
76 and x.field != "id"
77 and not x.field.endswith("_ptr"),
78 delta.changes,
79 )
80 )
81 return sorted(changes, key=lambda change: change.field)
84class VersionMixin(models.Model):
85 history = APISHistoricalRecords(
86 inherit=True,
87 bases=[
88 APISHistoryTableBase,
89 ],
90 custom_model_name=lambda x: f"Version{x}",
91 )
92 __history_date = None
94 @property
95 def _history_date(self):
96 return self.__history_date or timezone.now()
98 @_history_date.setter
99 def _history_date(self, value):
100 self.__history_date = value
101 pass
103 class Meta:
104 abstract = True
106 def get_history_url(self):
107 ct = ContentType.objects.get_for_model(self)
108 return reverse("apis_core:history:history", args=[ct, self.id])
110 def get_create_version_url(self):
111 ct = ContentType.objects.get_for_model(self)
112 return reverse("apis_core:history:add_new_history_version", args=[ct, self.id])
114 def _get_historical_relations(self):
115 ret = []
116 if "apis_core.relations" in settings.INSTALLED_APPS:
117 from apis_core.relations.utils import relation_content_types
119 ct = ContentType.objects.get_for_model(self)
121 rel_content_types = relation_content_types(any_model=type(self))
122 rel_models = [ct.model_class() for ct in rel_content_types]
123 rel_history_models = [
124 model for model in rel_models if issubclass(model, VersionMixin)
125 ]
127 for model in rel_history_models:
128 ret.append(
129 model.history.filter(
130 Q(subj_object_id=self.id, subj_content_type=ct)
131 | Q(obj_object_id=self.id, obj_content_type=ct)
132 ).order_by("history_id")
133 )
134 return ret
136 def get_history_data(self):
137 data = []
138 prev_entry = None
139 queries = self._get_historical_relations()
140 flatten_queries = [entry for query in queries for entry in query]
142 for entry in flatten_queries:
143 if prev_entry is not None:
144 if (
145 entry.history_date == prev_entry.history_date
146 and entry.history_user_id == prev_entry.history_user_id
147 ):
148 entry.history_type = prev_entry.history_type
149 data[-1] = entry
150 prev_entry = entry
151 continue
152 data.append(entry)
153 prev_entry = entry
154 data += [x for x in self.history.all()]
155 data = sorted(data, key=lambda x: x.history_date, reverse=True)
156 return data
158 def __init__(self, *args: Any, **kwargs: Any) -> None:
159 super().__init__(*args, **kwargs)