Skip to content

Commit

Permalink
Support customizing the history manager and historical queryset classes.
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-schilling committed Feb 13, 2024
1 parent f48b53d commit 1318d1c
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Unreleased
version is lower than 4.2 (gh-1261)
- Small performance optimization of the ``clean-duplicate_history`` command (gh-1015)
- Support Simplified Chinese translation (gh-1281)
- Support custom History `Manager` and `QuerySet` classes (gh-1280)

3.4.0 (2023-08-18)
------------------
Expand Down
62 changes: 62 additions & 0 deletions docs/historical_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,68 @@ IMPORTANT: Setting `custom_model_name` to `lambda x:f'{x}'` is not permitted.
An error will be generated and no history model created if they are the same.


Custom History Manager and Historical QuerySets
-----------------------------------------------

To manipulate the History Manager or the Historical QuerySet, you can specify the
``history_manager`` and ``historical_queryset`` options. Tht values must be subclasses
of ``simple_history.manager.HistoryManager`` and
``simple_history.manager.HistoricalQuerySet`` respectively.

Keep in mind, you can use either or both of these options. To understand the
difference between a `Manager` and QuerySet, see the Django `Manager documentation`_.


.. code-block:: python
from datetime import timedelta
from django.db import models
from django.utils import timezone
from simple_history.models import HistoricalRecords
from simple_history.manager import HistoryManager, HistoricalQuerySet
class HistoryQuestionManager(HistoryManager):
def published(self):
return self.filter(pub_date__lte=timezone.now())
class HistoryQuestionQuerySet(HistoricalQuerySet):
def question_prefixed(self):
return self.filter(question__startswith='Question: ')
class Question(models.Model):
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(
history_manager=HistoryQuestionManager,
historical_queryset=HistoryQuestionQuerySet,
)
queryset = Question.history.published().question_prefixed()
To reuse a `QuerySet` from the model, see the following code example:

.. code-block:: python
from datetime import timedelta
from django.db import models
from django.utils import timezone
from simple_history.models import HistoricalRecords
from simple_history.manager import HistoryManager, HistoricalQuerySet
class QuestionQuerySet(HistoricalQuerySet):
def question_prefixed(self):
return self.filter(question__startswith='Question: ')
class HistoryQuestionQuerySet(QuestionQuerySet, HistoricalQuerySet):
"""Redefine QuerySet with base class HistoricalQuerySet"""
class Question(models.Model):
pub_date = models.DateTimeField('date published')
history = HistoricalRecords(historical_queryset=HistoryQuestionQuerySet)
manager = models.Manager.from_queryset(HistoryQuestionQuerySet)()
.. _Manager documentation: https://docs.djangoproject.com/en/stable/topics/db/managers/


TextField as `history_change_reason`
------------------------------------

Expand Down
20 changes: 12 additions & 8 deletions simple_history/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,6 @@ def _instanceize(self):
setattr(historic, "_as_of", self._as_of)


class HistoryDescriptor:
def __init__(self, model):
self.model = model

def __get__(self, instance, owner):
return HistoryManager.from_queryset(HistoricalQuerySet)(self.model, instance)


class HistoryManager(models.Manager):
def __init__(self, model, instance=None):
super().__init__()
Expand Down Expand Up @@ -272,3 +264,15 @@ def bulk_history_create(
return self.model.objects.bulk_create(
historical_instances, batch_size=batch_size
)


class HistoryDescriptor:
def __init__(self, model, manager=HistoryManager, queryset=HistoricalQuerySet):
self.model = model
self.queryset_class = queryset
self.manager_class = manager

def __get__(self, instance, owner):
return self.manager_class.from_queryset(self.queryset_class)(
self.model, instance
)
17 changes: 15 additions & 2 deletions simple_history/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
from simple_history import utils

from . import exceptions
from .manager import SIMPLE_HISTORY_REVERSE_ATTR_NAME, HistoryDescriptor
from .manager import (
SIMPLE_HISTORY_REVERSE_ATTR_NAME,
HistoricalQuerySet,
HistoryDescriptor,
HistoryManager,
)
from .signals import (
post_create_historical_m2m_records,
post_create_historical_record,
Expand Down Expand Up @@ -100,6 +105,8 @@ def __init__(
user_db_constraint=True,
no_db_index=list(),
excluded_field_kwargs=None,
history_manager=HistoryManager,
historical_queryset=HistoricalQuerySet,
m2m_fields=(),
m2m_fields_model_field_name="_history_m2m_fields",
m2m_bases=(models.Model,),
Expand All @@ -122,6 +129,8 @@ def __init__(
self.user_setter = history_user_setter
self.related_name = related_name
self.use_base_model_db = use_base_model_db
self.history_manager = history_manager
self.historical_queryset = historical_queryset
self.m2m_fields = m2m_fields
self.m2m_fields_model_field_name = m2m_fields_model_field_name

Expand Down Expand Up @@ -215,7 +224,11 @@ def finalize(self, sender, **kwargs):
weak=False,
)

descriptor = HistoryDescriptor(history_model)
descriptor = HistoryDescriptor(
history_model,
manager=self.history_manager,
queryset=self.historical_queryset,
)
setattr(sender, self.manager_name, descriptor)
sender._meta.simple_history_manager_attribute = self.manager_name

Expand Down
20 changes: 20 additions & 0 deletions simple_history/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.urls import reverse

from simple_history import register
from simple_history.manager import HistoricalQuerySet, HistoryManager
from simple_history.models import HistoricalRecords, HistoricForeignKey

from .custom_user.models import CustomUser as User
Expand Down Expand Up @@ -155,6 +156,25 @@ class PollWithManyToManyCustomHistoryID(models.Model):
)


class PollQuerySet(HistoricalQuerySet):
def questions(self):
return self.filter(question__startswith="Question ")


class PollManager(HistoryManager):
def low_ids(self):
return self.filter(id__lte=3)


class PollWithQuerySetCustomizations(models.Model):
question = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")

history = HistoricalRecords(
history_manager=PollManager, historical_queryset=PollQuerySet
)


class HistoricalRecordsWithExtraFieldM2M(HistoricalRecords):
def get_extra_fields_m2m(self, model, through_model, fields):
extra_fields = super().get_extra_fields_m2m(model, through_model, fields)
Expand Down
37 changes: 37 additions & 0 deletions simple_history/tests/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
PollWithManyToManyCustomHistoryID,
PollWithManyToManyWithIPAddress,
PollWithNonEditableField,
PollWithQuerySetCustomizations,
PollWithSelfManyToMany,
PollWithSeveralManyToMany,
Province,
Expand Down Expand Up @@ -800,6 +801,42 @@ def test_history_with_unknown_field(self):
with self.assertNumQueries(0):
new_record.diff_against(old_record, excluded_fields=["unknown_field"])

def test_history_with_custom_queryset(self):
PollWithQuerySetCustomizations.objects.create(
id=1, pub_date=today, question="Question 1"
)
PollWithQuerySetCustomizations.objects.create(
id=2, pub_date=today, question="Low Id"
)
PollWithQuerySetCustomizations.objects.create(
id=10, pub_date=today, question="Random"
)

self.assertEqual(
set(
PollWithQuerySetCustomizations.history.low_ids().values_list(
"question", flat=True
)
),
{"Question 1", "Low Id"},
)
self.assertEqual(
set(
PollWithQuerySetCustomizations.history.questions().values_list(
"question", flat=True
)
),
{"Question 1"},
)
self.assertEqual(
set(
PollWithQuerySetCustomizations.history.low_ids()
.questions()
.values_list("question", flat=True)
),
{"Question 1"},
)


class GetPrevRecordAndNextRecordTestCase(TestCase):
def assertRecordsMatch(self, record_a, record_b):
Expand Down

0 comments on commit 1318d1c

Please sign in to comment.