Skip to content
Snippets Groups Projects
Commit c1d695ba authored by Christopher Hoskin's avatar Christopher Hoskin
Browse files

Import django-dirtyfields_1.3.orig.tar.gz

parent d6b1c7dd
No related branches found
Tags patched/1.3-1 upstream/1.3
No related merge requests found
......@@ -2,24 +2,15 @@ language: python
python:
- 2.7
- 3.4
- 3.5
env:
- TOXENV=django14
- TOXENV=django15
- TOXENV=django16
- TOXENV=django17
- TOXENV=django18
- TOXENV=django19
- TOXENV=django110
- TOXENV=coverage
- TOXENV=postgresql
matrix:
exclude:
- python: 3.4
env: TOXENV=django14
services:
- postgresql
......
ChangeLog
=========
.. _master:
master (unreleased)
-------------------
Up-to-date with 1.3
.. _v1.3:
1.3 (23/08/2017)
----------------
*New:*
- Drop support for unsupported Django versions: 1.4, 1.5, 1.6 and 1.7 series.
- Fixes issue with verbose mode when the object has not been yet saved in the database (MR #99). Thanks vapkarian.
- Add test coverage for Django 1.11.
- A new attribute :code:`FIELDS_TO_CHECK` has been added to :code:`DirtyFieldsMixin` to specify a limited set of fields to check.
*Bugfix:*
- Correctly handle :code:`ForeignKey.db_column` :code:`{}_id` in :code:`update_fields`. Thanks Hugo Smett.
- Fixes #111: Eliminate a memory leak.
- Handle deferred fields in :code:`update_fields`
.. _v1.2.1:
......@@ -10,6 +36,9 @@ ChangeLog
*New:*
- :code:`django-dirtyfields` is now tested with PostgreSQL, especially with specific fields
*Bugfix:*
- Fixes #80: Use of :code:`Field.rel` raises warnings from Django 1.9+
- Fixes #84: Use :code:`only()` in conjunction with 2 foreign keys triggers a recursion error
- Fixes #77: Shallow copy does not work with Django 1.9's JSONField
......@@ -46,7 +75,7 @@ ChangeLog
1.0.1 (2016-07-25)
------------------
*bugfix:*
*Bugfix:*
- Fixing a bug preventing :code:`django-dirtyfields` to work properly on models with custom primary keys.
......@@ -97,7 +126,7 @@ There is a backward-incompatibility on this version. Please read careful below.
0.8.1 (2015-12-08)
------------------
*bugfix:*
*Bugfix:*
- Not comparing fields that are deferred (:code:`only` method on :code:`QuerySet`).
- Being more tolerant when comparing values that can be on another type than expected.
......
......@@ -15,7 +15,7 @@ Django Dirty Fields
Tracking dirty fields on a Django model instance.
Dirty means that field in-memory and database values are different.
This package is compatible and tested with Django 1.4 to 1.10.
This package is compatible and tested with latest versions of Django (1.8, 1.9, 1.10, 1.11 series).
`Full documentation <http://django-dirtyfields.readthedocs.org/en/develop/>`_
......
......@@ -118,6 +118,30 @@ in the database needs to be updated with these form values.
It can have serious performance issues depending on your project.
Checking a limited set of model fields.
-------------------------------------
If you want to check a limited set of model fields, you should set ``FIELDS_TO_CHECK`` in your model inheriting from ``DirtyFieldsMixin``:
::
class TestModelWithSpecifiedFields(DirtyFieldsMixin, models.Model):
boolean1 = models.BooleanField(default=True)
boolean2 = models.BooleanField(default=True)
FIELDS_TO_CHECK = ['boolean1']
>>> from tests.models import TestModelWithSpecifiedFields
>>> tm = TestModelWithSpecifiedFields.objects.create()
>>> tm.boolean1 = False
>>> tm.boolean2 = False
>>> tm.get_dirty_fields()
{'boolean1': True}
This can be used in order to increase performance.
Saving dirty fields.
----------------------------
If you want to only save dirty fields from an instance in the database (only these fields will be involved in SQL query), you can use ``save_dirty_fields`` method.
......
Django>=1.4
Django>=1.8
pytz>=2015.7
\ No newline at end of file
......@@ -7,7 +7,7 @@ def listify(filename):
setup(
name="django-dirtyfields",
version="1.2.1",
version="1.3",
url='http://github.com/romgar/django-dirtyfields',
license='BSD',
description=("Tracking dirty fields on a Django model instance "
......
import sys
import django
from django.db.models import signals
from django.db.models.query_utils import DeferredAttribute
def get_m2m_with_model(given_model):
......@@ -15,49 +13,6 @@ def get_m2m_with_model(given_model):
]
def is_db_expression(value):
try:
# django < 1.8
from django.db.models.expressions import ExpressionNode
return isinstance(value, ExpressionNode)
except ImportError:
# django >= 1.8 (big refactoring in Lookup/Expressions/Transforms)
from django.db.models.expressions import BaseExpression, Combinable
return isinstance(value, (BaseExpression, Combinable))
def is_deferred(instance, field):
if django.VERSION < (1, 8):
attr = instance.__class__.__dict__.get(field.attname)
return isinstance(attr, DeferredAttribute)
else:
return field.get_attname() in instance.get_deferred_fields()
def save_specific_fields(instance, fields_list):
update_fields = fields_list.keys()
if django.VERSION >= (1, 5):
instance.save(update_fields=update_fields)
else:
# dirtyfields is by default returning dirty fields with their old value
# We should pass the new value(s) to update the database
new_fields_list = {field_name: getattr(instance, field_name)
for field_name, field_value in fields_list.items()}
# dirtyfield is based on post_save signal to save last database value in memory.
# As we need to manually launch post_save signal, we also launch pre_save
# to be coherent with django 'classic' save signals.
signals.pre_save.send(sender=instance.__class__, instance=instance, update_fields=update_fields)
# django < 1.5 does not support update_fields option on save method
instance.__class__.objects.filter(pk=instance.pk).update(**new_fields_list)
# dirtyfield is based on post_save signal to save last database value in memory.
# As update() method does not trigger this signal, we launch it explicitly.
signals.post_save.send(sender=instance.__class__, instance=instance, update_fields=update_fields)
def is_buffer(value):
if sys.version_info < (3, 0, 0):
return isinstance(value, buffer)
......
......@@ -2,11 +2,12 @@
from copy import deepcopy
from django.core.exceptions import ValidationError
from django.db.models.expressions import BaseExpression
from django.db.models.expressions import Combinable
from django.db.models.signals import post_save, m2m_changed
from .compare import raw_compare, compare_states
from .compat import (is_db_expression, save_specific_fields,
is_deferred, is_buffer, get_m2m_with_model, remote_field)
from .compat import is_buffer, get_m2m_with_model, remote_field
class DirtyFieldsMixin(object):
......@@ -16,10 +17,12 @@ class DirtyFieldsMixin(object):
# https://github.com/romgar/django-dirtyfields/issues/73
ENABLE_M2M_CHECK = False
FIELDS_TO_CHECK = None
def __init__(self, *args, **kwargs):
super(DirtyFieldsMixin, self).__init__(*args, **kwargs)
post_save.connect(
reset_state, sender=self.__class__,
reset_state, sender=self.__class__, weak=False,
dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format(
name=self.__class__.__name__))
if self.ENABLE_M2M_CHECK:
......@@ -29,7 +32,7 @@ class DirtyFieldsMixin(object):
def _connect_m2m_relations(self):
for m2m_field, model in get_m2m_with_model(self.__class__):
m2m_changed.connect(
reset_state, sender=remote_field(m2m_field).through,
reset_state, sender=remote_field(m2m_field).through, weak=False,
dispatch_uid='{name}-DirtyFieldsMixin-sweeper-m2m'.format(
name=self.__class__.__name__))
......@@ -37,6 +40,9 @@ class DirtyFieldsMixin(object):
all_field = {}
for field in self._meta.fields:
if self.FIELDS_TO_CHECK and (field.get_attname() not in self.FIELDS_TO_CHECK):
continue
if field.primary_key and not include_primary_key:
continue
......@@ -44,13 +50,13 @@ class DirtyFieldsMixin(object):
if not check_relationship:
continue
if is_deferred(self, field):
if field.get_attname() in self.get_deferred_fields():
continue
field_value = getattr(self, field.attname)
# If current field value is an expression, we are not evaluating it
if is_db_expression(field_value):
if isinstance(field_value, (BaseExpression, Combinable)):
continue
try:
......@@ -71,15 +77,17 @@ class DirtyFieldsMixin(object):
return all_field
def _as_dict_m2m(self):
m2m_fields = {}
if self.pk:
m2m_fields = dict([
(f.attname, set([
obj.pk for obj in getattr(self, f.attname).all()
]))
for f, model in get_m2m_with_model(self.__class__)
])
return m2m_fields
return {}
for f, model in get_m2m_with_model(self.__class__):
if self.FIELDS_TO_CHECK and (f.attname not in self.FIELDS_TO_CHECK):
continue
m2m_fields[f.attname] = set([obj.pk for obj in getattr(self, f.attname).all()])
return m2m_fields
def get_dirty_fields(self, check_relationship=False, check_m2m=None, verbose=False):
if self._state.adding:
......@@ -87,6 +95,9 @@ class DirtyFieldsMixin(object):
# for consistency (see https://github.com/romgar/django-dirtyfields/issues/65 for more details)
pk_specified = self.pk is not None
initial_dict = self._as_dict(check_relationship, include_primary_key=pk_specified)
if verbose:
initial_dict = {key: {'saved': None, 'current': value}
for key, value in initial_dict.items()}
return initial_dict
if check_m2m is not None and not self.ENABLE_M2M_CHECK:
......@@ -114,7 +125,7 @@ class DirtyFieldsMixin(object):
def save_dirty_fields(self):
dirty_fields = self.get_dirty_fields(check_relationship=True)
save_specific_fields(self, dirty_fields)
self.save(update_fields=dirty_fields.keys())
def reset_state(sender, instance, **kwargs):
......@@ -123,8 +134,14 @@ def reset_state(sender, instance, **kwargs):
update_fields = kwargs.pop('update_fields', {})
new_state = instance._as_dict(check_relationship=True)
if update_fields:
for field in update_fields:
instance._original_state[field] = new_state[field]
for field_name in update_fields:
field = sender._meta.get_field(field_name)
if field.get_attname() in instance.get_deferred_fields():
continue
instance._original_state[field.name] = new_state[field.name]
else:
instance._original_state = new_state
if instance.ENABLE_M2M_CHECK:
......
......@@ -117,3 +117,17 @@ if is_postgresql_env_with_json_field():
class TestModelWithJSONField(DirtyFieldsMixin, models.Model):
json_field = JSONField()
class TestModelWithSpecifiedFields(DirtyFieldsMixin, models.Model):
boolean1 = models.BooleanField(default=True)
boolean2 = models.BooleanField(default=True)
FIELDS_TO_CHECK = ['boolean1']
class TestModelWithM2MAndSpecifiedFields(DirtyFieldsMixin, models.Model):
m2m1 = models.ManyToManyField(TestModel)
m2m2 = models.ManyToManyField(TestModel)
ENABLE_M2M_CHECK = True
FIELDS_TO_CHECK = ['m2m1']
......@@ -153,3 +153,13 @@ def test_verbose_mode():
assert tm.get_dirty_fields(verbose=True) == {
'boolean': {'saved': True, 'current': False}
}
@pytest.mark.django_db
def test_verbose_mode_on_adding():
tm = TestModel()
assert tm.get_dirty_fields(verbose=True) == {
'boolean': {'saved': None, 'current': True},
'characters': {'saved': None, 'current': u''}
}
import resource
import pytest
from .models import TestModel as DirtyMixinModel
pytestmark = pytest.mark.django_db
def test_rss_usage():
DirtyMixinModel()
rss_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
for _ in range(1000):
DirtyMixinModel()
rss_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
assert rss_2 == rss_1, 'There is a memory leak!'
......@@ -3,7 +3,7 @@ import unittest
import django
import pytest
from .models import TestModel, TestMixedFieldsModel
from .models import TestModel, TestMixedFieldsModel, TestModelWithForeignKey
from .utils import assert_number_of_queries_on_regex
......@@ -55,7 +55,6 @@ def test_save_dirty_related_field():
assert TestMixedFieldsModel.objects.get(pk=tmfm.pk).fkey_id == tm1.id
@unittest.skipIf(django.VERSION < (1, 5), "Django 1.4 doesn't support update_fields param on save()")
@pytest.mark.django_db
def test_save_only_specific_fields_should_let_other_fields_dirty():
tm = TestModel.objects.create(boolean=True, characters='dummy')
......@@ -67,3 +66,40 @@ def test_save_only_specific_fields_should_let_other_fields_dirty():
# 'characters' field should still be dirty, update_fields was only saving the 'boolean' field in the db
assert tm.get_dirty_fields() == {'characters': 'dummy'}
@pytest.mark.django_db
def test_handle_foreignkeys_id_field_in_update_fields():
tm1 = TestModel.objects.create(boolean=True, characters='dummy')
tm2 = TestModel.objects.create(boolean=True, characters='dummy')
tmwfk = TestModelWithForeignKey.objects.create(fkey=tm1)
tmwfk.fkey = tm2
assert tmwfk.get_dirty_fields(check_relationship=True) == {'fkey': tm1.pk}
tmwfk.save(update_fields=['fkey_id'])
assert tmwfk.get_dirty_fields(check_relationship=True) == {}
@pytest.mark.django_db
def test_correctly_handle_foreignkeys_id_field_in_update_fields():
tm1 = TestModel.objects.create(boolean=True, characters='dummy')
tm2 = TestModel.objects.create(boolean=True, characters='dummy')
tmwfk = TestModelWithForeignKey.objects.create(fkey=tm1)
tmwfk.fkey_id = tm2.pk
assert tmwfk.get_dirty_fields(check_relationship=True) == {'fkey': tm1.pk}
tmwfk.save(update_fields=['fkey'])
assert tmwfk.get_dirty_fields(check_relationship=True) == {}
@pytest.mark.django_db
def test_save_deferred_field_with_update_fields():
TestModel.objects.create()
tm = TestModel.objects.defer('boolean').first()
tm.boolean = False
# Test that providing a deferred field to the update_fields
# save parameter doesn't raise a KeyError anymore.
tm.save(update_fields=['boolean'])
import pytest
from .models import TestModel, TestModelWithSpecifiedFields, TestModelWithM2MAndSpecifiedFields
@pytest.mark.django_db
def test_dirty_fields_on_model_with_specified_fields():
tm = TestModelWithSpecifiedFields.objects.create()
tm.boolean1 = False
tm.boolean2 = False
# boolean1 is tracked, boolean2 isn`t tracked
assert tm.get_dirty_fields() == {'boolean1': True}
@pytest.mark.django_db
def test_dirty_fields_on_model_with_m2m_and_specified_fields():
tm = TestModelWithM2MAndSpecifiedFields.objects.create()
tm2 = TestModel.objects.create()
tm.m2m1.add(tm2)
tm.m2m2.add(tm2)
# m2m1 is tracked, m2m2 isn`t tracked
assert tm.get_dirty_fields(check_m2m={'m2m1': set([])}) == {'m2m1': set([tm2.id])}
assert tm.get_dirty_fields(check_m2m={'m2m2': set([])}) == {}
[tox]
envlist = django14,django15,django16,django17,django18,django19,django110,coverage,postgresql
envlist = django18,django19,django110,django111,coverage,postgresql
[testenv]
setenv =
......@@ -7,31 +7,11 @@ setenv =
commands =
py.test --ds=tests.django_settings -v
deps =
pytest==2.7.1
pytest-django==2.8.0
pytest==3.0.6
pytest-django==3.1.2
jsonfield==1.0.3
pytz
[testenv:django14]
deps =
django>=1.4,<1.4.99
{[testenv]deps}
[testenv:django15]
deps =
django>=1.5,<1.5.99
{[testenv]deps}
[testenv:django16]
deps =
django>=1.6,<1.6.99
{[testenv]deps}
[testenv:django17]
deps =
django>=1.7,<1.7.99
{[testenv]deps}
[testenv:django18]
deps =
django>=1.8,<1.8.99
......@@ -47,13 +27,18 @@ deps =
django>=1.10,<1.10.99
{[testenv]deps}
[testenv:django111]
deps =
django>=1.11,<1.11.99
{[testenv]deps}
[testenv:postgresql]
setenv =
PYTHONPATH = {toxinidir}
commands =
py.test --ds=tests.postgresql_django_settings -v
deps =
django>=1.10,<1.10.99
django>=1.11,<1.11.99
psycopg2
{[testenv]deps}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment