Skip to content

Commit 6b113e3

Browse files
nick-traegerRoss Mechanic
authored andcommitted
added the possibility to create a relation to the original model (#536)
* added the possibility to create a relation to the original model * added some tests for related_name * reformatted using black * added myself to AUTHORS.rst * Preventing the related name from being the same as the history manager * Added test to check related name for equality to manager * Rearranged myself in alphabetical order * Added changes to CHANGES.rst * Added some documentation * db_constraint added and do not set instance of history_relation when deleting * Keeping relation after delete * Test if the relation works again after deleting and restoring the object * Revert test extended * Updated version in CHANGES to unreleased
1 parent de0c300 commit 6b113e3

File tree

7 files changed

+120
-0
lines changed

7 files changed

+120
-0
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Authors
6868
- Mike Spainhower
6969
- Nathan Villagaray-Carski (`ncvc <https://github.com/ncvc>`_)
7070
- Nianpeng Li
71+
- Nick Träger
7172
- Phillip Marshall
7273
- Rajesh Pappula
7374
- Ray Logel

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changes
44
- remove reference to vendored library ``django.utils.six`` in favor of ``six`` (gh-526)
55
- Fixed hardcoded history manager (gh-540)
66

7+
Unreleased
8+
----------
9+
- Added the possibility to create a relation to the original model
10+
711
2.7.0 (2019-01-16)
812
------------------
913
- Add support for ``using`` chained manager method and save/delete keyword argument (gh-507)

docs/querying_history.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,18 @@ If you want to save a model without a historical record, you can use the followi
147147
148148
poll = Poll(question='something')
149149
poll.save_without_historical_record()
150+
151+
152+
Filtering data using a relationship to the model
153+
------------------------------------------------
154+
155+
To filter changes to the data, a relationship to the history can be established. For example, all data records in which a particular user was involved.
156+
157+
.. code-block:: python
158+
159+
class Poll(models.Model):
160+
question = models.CharField(max_length=200)
161+
log = HistoricalRecords(related_name='history')
162+
163+
164+
Poll.objects.filter(history__history_user=4)

simple_history/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ class NotHistoricalModelError(TypeError):
1313
"""No related history model found."""
1414

1515
pass
16+
17+
18+
class RelatedNameConflictError(Exception):
19+
"""Related name conflicting with history manager"""
20+
21+
pass

simple_history/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def __init__(
7878
history_user_id_field=None,
7979
history_user_getter=_history_user_getter,
8080
history_user_setter=_history_user_setter,
81+
related_name=None,
8182
):
8283
self.user_set_verbose_name = verbose_name
8384
self.user_related_name = user_related_name
@@ -93,6 +94,7 @@ def __init__(
9394
self.user_id_field = history_user_id_field
9495
self.user_getter = history_user_getter
9596
self.user_setter = history_user_setter
97+
self.related_name = related_name
9698

9799
if excluded_fields is None:
98100
excluded_fields = []
@@ -315,6 +317,23 @@ def _get_history_user_fields(self):
315317

316318
return history_user_fields
317319

320+
def _get_history_related_field(self, model):
321+
if self.related_name:
322+
if self.manager_name == self.related_name:
323+
raise exceptions.RelatedNameConflictError(
324+
"The related name must not be called like the history manager."
325+
)
326+
return {
327+
"history_relation": models.ForeignKey(
328+
model,
329+
on_delete=models.DO_NOTHING,
330+
related_name=self.related_name,
331+
db_constraint=False,
332+
)
333+
}
334+
else:
335+
return {}
336+
318337
def get_extra_fields(self, model, fields):
319338
"""Return dict of extra fields added to the historical record model"""
320339

@@ -387,6 +406,7 @@ def get_prev_record(self):
387406
),
388407
}
389408

409+
extra_fields.update(self._get_history_related_field(model))
390410
extra_fields.update(self._get_history_user_fields())
391411

392412
return extra_fields
@@ -432,6 +452,10 @@ def create_historical_record(self, instance, history_type, using=None):
432452
for field in self.fields_included(instance):
433453
attrs[field.attname] = getattr(instance, field.attname)
434454

455+
relation_field = getattr(manager.model, "history_relation", None)
456+
if relation_field is not None:
457+
attrs["history_relation"] = instance
458+
435459
history_instance = manager.model(
436460
history_date=history_date,
437461
history_type=history_type,

simple_history/tests/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,3 +567,8 @@ class ForeignKeyToSelfModel(models.Model):
567567
"self", null=True, related_name="+", on_delete=models.CASCADE
568568
)
569569
history = HistoricalRecords()
570+
571+
572+
class Street(models.Model):
573+
name = models.CharField(max_length=150)
574+
log = HistoricalRecords(related_name="history")

simple_history/tests/tests/test_models.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from django.test import TestCase, override_settings
1616
from django.urls import reverse
1717

18+
from simple_history import register
19+
from simple_history.exceptions import RelatedNameConflictError
1820
from simple_history.models import HistoricalRecords, ModelChange
1921
from simple_history.signals import pre_create_historical_record
2022
from simple_history.tests.custom_user.models import CustomUser
@@ -74,6 +76,7 @@
7476
Series,
7577
SeriesWork,
7678
State,
79+
Street,
7780
Temperature,
7881
UUIDDefaultModel,
7982
UUIDModel,
@@ -1413,3 +1416,65 @@ def test_history_user_does_not_exist(self):
14131416

14141417
self.assertEqual(user_id, instance.history.first().history_user_id)
14151418
self.assertIsNone(instance.history.first().history_user)
1419+
1420+
1421+
class RelatedNameTest(TestCase):
1422+
def setUp(self):
1423+
self.user_one = get_user_model().objects.create(
1424+
username="username_one", email="[email protected]", password="top_secret"
1425+
)
1426+
self.user_two = get_user_model().objects.create(
1427+
username="username_two", email="[email protected]", password="top_secret"
1428+
)
1429+
1430+
self.one = Street(name="Test Street")
1431+
self.one._history_user = self.user_one
1432+
self.one.save()
1433+
1434+
self.two = Street(name="Sesame Street")
1435+
self.two._history_user = self.user_two
1436+
self.two.save()
1437+
1438+
self.one.name = "ABC Street"
1439+
self.one._history_user = self.user_two
1440+
self.one.save()
1441+
1442+
def test_relation(self):
1443+
self.assertEqual(self.one.history.count(), 2)
1444+
self.assertEqual(self.two.history.count(), 1)
1445+
1446+
def test_filter(self):
1447+
self.assertEqual(
1448+
Street.objects.filter(history__history_user=self.user_one.pk).count(), 1
1449+
)
1450+
self.assertEqual(
1451+
Street.objects.filter(history__history_user=self.user_two.pk).count(), 2
1452+
)
1453+
1454+
def test_name_equals_manager(self):
1455+
with self.assertRaises(RelatedNameConflictError):
1456+
register(Place, manager_name="history", related_name="history")
1457+
1458+
def test_deletion(self):
1459+
self.two.delete()
1460+
1461+
self.assertEqual(Street.log.filter(history_relation=2).count(), 2)
1462+
self.assertEqual(Street.log.count(), 4)
1463+
1464+
def test_revert(self):
1465+
id = self.one.pk
1466+
1467+
self.one.delete()
1468+
self.assertEqual(
1469+
Street.objects.filter(history__history_user=self.user_one.pk).count(), 0
1470+
)
1471+
self.assertEqual(Street.objects.filter(pk=id).count(), 0)
1472+
1473+
old = Street.log.filter(id=id).first()
1474+
old.history_object.save()
1475+
self.assertEqual(
1476+
Street.objects.filter(history__history_user=self.user_one.pk).count(), 1
1477+
)
1478+
1479+
self.one = Street.objects.get(pk=id)
1480+
self.assertEqual(self.one.history.count(), 4)

0 commit comments

Comments
 (0)