Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new FieldTracker that works well with foreign keys #52

Merged
merged 15 commits into from
Jun 3, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ CHANGES
tip (unreleased)
----------------

- Introduced ``FieldTracker`` as replacement for ``ModelTracker``, which is now
deprecated.

- ``PassThroughManager.for_queryset_class()`` no longer ignores superclass
``get_query_set``. Thanks Andy Freeland.

Expand Down
62 changes: 48 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Assignment to ``a.body`` is equivalent to assignment to
``a.body.content``.

.. note::

a.body.excerpt is only updated when a.save() is called


Expand Down Expand Up @@ -418,29 +419,58 @@ directly on the manager:
Post.objects.by_author(user=request.user).unpublished()


ModelTracker
FieldTracker
============

A ``ModelTracker`` can be added to a model to track changes in model fields. A
``ModelTracker`` allows querying for field changes since a model instance was
last saved. An example of applying ``ModelTracker`` to a model:
A ``FieldTracker`` can be added to a model to track changes in model fields. A
``FieldTracker`` allows querying for field changes since a model instance was
last saved. An example of applying ``FieldTracker`` to a model:

.. code-block:: python

from django.db import models
from model_utils import ModelTracker
from model_utils import FieldTracker

class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()

tracker = ModelTracker()
tracker = FieldTracker()

.. note::

``django-model-utils`` 1.3.0 introduced the ``ModelTracker`` object for
tracking changes to model field values. Unfortunately ``ModelTracker``
suffered from some serious flaws in its handling of ``ForeignKey`` fields,
potentially resulting in many extra database queries if a ``ForeignKey``
field was tracked. In order to avoid breaking API backwards-compatibility,
``ModelTracker`` retains the previous behavior but is deprecated, and
``FieldTracker`` has been introduced to provide better ``ForeignKey``
handling. All uses of ``ModelTracker`` should be replaced by
``FieldTracker``.

Summary of differences between ``ModelTracker`` and ``FieldTracker``:

* The previous value returned for a tracked ``ForeignKey`` field will now
be the raw ID rather than the full object (avoiding extra database
queries). (GH-43)

Accessing a model tracker
* The ``changed()`` method no longer returns the empty dictionary for all
unsaved instances; rather, ``None`` is considered to be the initial value
of all fields if the model has never been saved, thus ``changed()`` on an
unsaved instance will return a dictionary containing all fields whose
current value is not ``None``.

* The ``has_changed()`` method no longer crashes after an object's first
save. (GH-53).


Accessing a field tracker
-------------------------

There are multiple methods available for checking for changes in model fields.


previous
~~~~~~~~
Returns the value of the given field during the last save:
Expand All @@ -454,6 +484,7 @@ Returns the value of the given field during the last save:

Returns ``None`` when the model instance isn't saved yet.


has_changed
~~~~~~~~~~~
Returns ``True`` if the given field has changed since the last save:
Expand All @@ -467,7 +498,9 @@ Returns ``True`` if the given field has changed since the last save:
>>> a.tracker.has_changed('body')
False

Returns ``True`` if the model instance hasn't been saved yet.
The ``has_changed`` method relies on ``previous`` to determine whether a
field's values has changed.


changed
~~~~~~~
Expand All @@ -482,25 +515,26 @@ and the values of the fields during the last save:
>>> a.tracker.changed()
{'title': 'First Post', 'body': ''}

Returns ``{}`` if the model instance hasn't been saved yet.
The ``changed`` method relies on ``has_changed`` to determine which fields
have changed.


Tracking specific fields
------------------------

A fields parameter can be given to ``ModelTracker`` to limit model tracking to
the specific fields:
A fields parameter can be given to ``FieldTracker`` to limit tracking to
specific fields:

.. code-block:: python

from django.db import models
from model_utils import ModelTracker
from model_utils import FieldTracker

class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()

title_tracker = ModelTracker(fields=['title'])
title_tracker = FieldTracker(fields=['title'])

An example using the model specified above:

Expand All @@ -509,5 +543,5 @@ An example using the model specified above:
>>> a = Post.objects.create(title='First Post')
>>> a.body = 'First post!'
>>> a.title_tracker.changed()
{}
{'title': None}

2 changes: 1 addition & 1 deletion model_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .choices import Choices
from .tracker import ModelTracker
from .tracker import FieldTracker, ModelTracker
54 changes: 51 additions & 3 deletions model_utils/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.utils.translation import ugettext_lazy as _

from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.tracker import ModelTracker
from model_utils.tracker import FieldTracker, ModelTracker
from model_utils.managers import QueryManager, InheritanceManager, PassThroughManager
from model_utils.fields import SplitField, MonitorField, StatusField
from model_utils import Choices
Expand Down Expand Up @@ -235,20 +235,68 @@ class Tracked(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

tracker = ModelTracker()
tracker = FieldTracker()


class TrackedFK(models.Model):
fk = models.ForeignKey('Tracked')

tracker = FieldTracker()
custom_tracker = FieldTracker(fields=['fk_id'])
custom_tracker_without_id = FieldTracker(fields=['fk'])


class TrackedNotDefault(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

name_tracker = ModelTracker(fields=['name'])
name_tracker = FieldTracker(fields=['name'])


class TrackedNonFieldAttr(models.Model):
number = models.FloatField()

@property
def rounded(self):
return round(self.number) if self.number is not None else None

tracker = FieldTracker(fields=['rounded'])


class TrackedMultiple(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

name_tracker = FieldTracker(fields=['name'])
number_tracker = FieldTracker(fields=['number'])


class ModelTracked(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

tracker = ModelTracker()


class ModelTrackedFK(models.Model):
fk = models.ForeignKey('ModelTracked')

tracker = ModelTracker()
custom_tracker = ModelTracker(fields=['fk_id'])
custom_tracker_without_id = ModelTracker(fields=['fk'])


class ModelTrackedNotDefault(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

name_tracker = ModelTracker(fields=['name'])


class ModelTrackedMultiple(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()

name_tracker = ModelTracker(fields=['name'])
number_tracker = ModelTracker(fields=['number'])

Expand Down
Loading