pax_global_header00006660000000000000000000000064131473030550014513gustar00rootroot0000000000000052 comment=4a106838ffc30337bffdc23c811d4f68128d1f97 django-dirtyfields-1.3/000077500000000000000000000000001314730305500151405ustar00rootroot00000000000000django-dirtyfields-1.3/.gitignore000066400000000000000000000001341314730305500171260ustar00rootroot00000000000000*.pyc ve *.egg-info *.db docs/_build/* docs/_template/* docs/_static/* .tox .cache .coveragedjango-dirtyfields-1.3/.travis.yml000066400000000000000000000013301314730305500172460ustar00rootroot00000000000000language: python python: - 2.7 - 3.5 env: - TOXENV=django18 - TOXENV=django19 - TOXENV=django110 - TOXENV=coverage - TOXENV=postgresql services: - postgresql addons: postgresql: "9.4" before_script: - psql -c 'create database dirtyfields_test;' -U postgres script: - tox install: - pip install tox - pip install coveralls - pip install -e . after_success: - if test "$TOXENV" = "coverage"; then coveralls; fi deploy: provider: pypi user: smn password: secure: "sXlDQNG8p+F4y8TKbRVw44uBCzECecgWQLNYcJBObMPoPbp64Ux488kM5RhbYhof0H1W850CKZzW66GQAjay0HS8tp9nPXg35xInPongLFuPzapoOdHtJZa5ub9QnpIs6LifJ/zXP5YDqh8ZUyoD+oFMvMFKjBkVB/NIYMYkyEM=" on: tags: true all_branches: true django-dirtyfields-1.3/AUTHORS000066400000000000000000000001361314730305500162100ustar00rootroot00000000000000Romain Garrigues Simon de Haan django-dirtyfields-1.3/CLASSIFIERS.txt000066400000000000000000000006321314730305500174510ustar00rootroot00000000000000Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Framework :: Django Topic :: Software Development :: Libraries :: Python Modules django-dirtyfields-1.3/ChangeLog.rst000066400000000000000000000126771314730305500175360ustar00rootroot00000000000000ChangeLog ========= .. _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: 1.2.1 (2016-11-16) ------------------ *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 - Fixes #88: :code:`get_dirty_fields` on a newly-created model does not work if pk is specified - Fixes #90: Unmark dirty fields only listed in :code:`update_fields` .. _v1.2: 1.2 (2016-08-11) ---------------- *New:* - :code:`django-dirtyfields` is now compatible with Django 1.10 series (deferred field handling has been updated). .. _v1.1: 1.1 (2016-08-04) ---------------- *New:* - A new attribute :code:`ENABLE_M2M_CHECK` has been added to :code:`DirtyFieldsMixin` to enable/disable m2m check functionality. This parameter is set to :code:`False` by default. IMPORTANT: backward incompatibility with v1.0.x series. If you were using :code:`check_m2m` parameter to check m2m relations, you should now add :code:`ENABLE_M2M_CHECK = True` to these models inheriting from :code:`DirtyFieldsMixin`. Check the documentation to see more details/examples. .. _v1.0.1: 1.0.1 (2016-07-25) ------------------ *Bugfix:* - Fixing a bug preventing :code:`django-dirtyfields` to work properly on models with custom primary keys. .. _v1.0: 1.0 (2016-06-26) ---------------- After several years of existence, django-dirty-fields is mature enough to switch to 1.X version. There is a backward-incompatibility on this version. Please read careful below. *New:* - IMPORTANT: :code:`get_dirty_fields` is now more consistent for models not yet saved in the database. :code:`get_dirty_fields` is, in that situation, always returning ALL fields, where it was before returning various results depending on how you initialised your model. It may affect you specially if you are using :code:`get_dirty_fields` in a :code:`pre_save` receiver. See more details at https://github.com/romgar/django-dirtyfields/issues/65. - Adding compatibility for old _meta API, deprecated in Django `1.10` version and now replaced by an official API. - General test cleaning. .. _v0.9: 0.9 (2016-06-18) ---------------- *New:* - Adding Many-to-Many fields comparison method :code:`check_m2m` in :code:`DirtyFieldsMixin`. - Adding :code:`verbose` parameter in :code:`get_dirty_fields` method to get old AND new field values. .. _v0.8.2: 0.8.2 (2016-03-19) ------------------ *New:* - Adding field comparison method :code:`compare_function` in :code:`DirtyFieldsMixin`. - Also adding a specific comparison function :code:`timezone_support_compare` to handle different Datetime situations. .. _v0.8.1: 0.8.1 (2015-12-08) ------------------ *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. .. _v0.8: 0.8 (2015-10-30) ---------------- *New:* - Adding :code:`save_dirty_fields` method to save only dirty fields in the database. .. _v0.7: 0.7 (2015-06-18) ---------------- *New:* - Using :code:`copy` to properly track dirty fields on complex fields. - Using :code:`py.test` for tests launching. .. _v0.6.1: 0.6.1 (2015-06-14) ------------------ *Bugfix:* - Preventing django db expressions to be evaluated when testing dirty fields (#39). .. _v0.6: 0.6 (2015-06-11) ---------------- *New:* - Using :code:`to_python` to avoid false positives when dealing with model fields that internally convert values (#4) *Bugfix:* - Using :code:`attname` instead of :code:`name` on fields to avoid massive useless queries on ForeignKey fields (#34). For this kind of field, :code:`get_dirty_fields()` is now returning instance id, instead of instance itself. .. _v0.5: 0.5 (2015-05-06) ---------------- *New:* - Adding code compatibility for python3, - Launching travis-ci tests on python3, - Using :code:`tox` to launch tests on Django 1.5, 1.6, 1.7 and 1.8 versions, - Updating :code:`runtests.py` test script to run properly on every Django version. *Bugfix:* - Catching :code:`Error` when trying to get foreign key object if not existing (#32). .. _v0.4.1: 0.4.1 (2015-04-08) ------------------ *Bugfix:* - Removing :code:`model_to_form` to avoid bug when using models that have :code:`editable=False` fields. .. _v0.4: 0.4 (2015-03-31) ---------------- *New:* - Adding :code:`check_relationship` parameter on :code:`is_dirty` and :code:`get_dirty_field` methods to also check foreign key values. django-dirtyfields-1.3/LICENSE000066400000000000000000000030501314730305500161430ustar00rootroot00000000000000Copyright (c) Praekelt Foundation and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the Praekelt Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.django-dirtyfields-1.3/MANIFEST.in000066400000000000000000000001231314730305500166720ustar00rootroot00000000000000include LICENSE include README.rst include CLASSIFIERS.txt include requirements.txtdjango-dirtyfields-1.3/README.rst000066400000000000000000000042421314730305500166310ustar00rootroot00000000000000=================== Django Dirty Fields =================== .. image:: https://badges.gitter.im/Join%20Chat.svg :alt: Join the chat at https://gitter.im/romgar/django-dirtyfields :target: https://gitter.im/romgar/django-dirtyfields?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. image:: https://travis-ci.org/romgar/django-dirtyfields.svg?branch=develop :target: https://travis-ci.org/romgar/django-dirtyfields?branch=develop .. image:: https://coveralls.io/repos/romgar/django-dirtyfields/badge.svg?branch=develop :target: https://coveralls.io/r/romgar/django-dirtyfields?branch=develop .. image:: http://readthedocs.org/projects/django-dirtyfields/badge/?version=develop :target: http://django-dirtyfields.readthedocs.org/en/develop/?badge=develop 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 latest versions of Django (1.8, 1.9, 1.10, 1.11 series). `Full documentation `_ Install ======= :: $ pip install django-dirtyfields Usage ===== To use ``django-dirtyfields``, you need to: - Inherit from ``DirtyFieldMixin`` in the Django model you want to track. :: from django.db import models from dirtyfields import DirtyFieldsMixin class TestModel(DirtyFieldsMixin, models.Model): """A simple test model to test dirty fields mixin with""" boolean = models.BooleanField(default=True) characters = models.CharField(blank=True, max_length=80) - Use one of these 2 functions on a model instance to know if this instance is dirty, and get the dirty fields: * is\_dirty() * get\_dirty\_fields() Example ------- :: >>> from tests.models import TestModel >>> tm = TestModel.objects.create(boolean=True,characters="testing") >>> tm.is_dirty() False >>> tm.get_dirty_fields() {} >>> tm.boolean = False >>> tm.is_dirty() True >>> tm.get_dirty_fields() {'boolean': True} Consult the `full documentation `_ for more informations. django-dirtyfields-1.3/docs/000077500000000000000000000000001314730305500160705ustar00rootroot00000000000000django-dirtyfields-1.3/docs/Makefile000066400000000000000000000164411314730305500175360ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-dirtyfields.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-dirtyfields.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-dirtyfields" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-dirtyfields" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." django-dirtyfields-1.3/docs/_ext/000077500000000000000000000000001314730305500170275ustar00rootroot00000000000000django-dirtyfields-1.3/docs/_ext/djangodocs.py000066400000000000000000000016171314730305500215210ustar00rootroot00000000000000 def setup(app): """ Mandatory to cross ref any non-default sphinx behaviours defined in Django Thanks http://reinout.vanrees.org/weblog/2012/12/01/django-intersphinx.html We can then use :django:settings:`ROOT_URLCONF` for example (We then avoid ERROR: Unknown interpreted text role "django:settings") """ app.add_crossref_type( directivename="setting", rolename="setting", indextemplate="pair: %s; setting", ) app.add_crossref_type( directivename="templatetag", rolename="ttag", indextemplate="pair: %s; template tag" ) app.add_crossref_type( directivename="templatefilter", rolename="tfilter", indextemplate="pair: %s; template filter" ) app.add_crossref_type( directivename="fieldlookup", rolename="lookup", indextemplate="pair: %s; field lookup type", ) django-dirtyfields-1.3/docs/conf.py000066400000000000000000000230051314730305500173670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # django-dirtyfields documentation build configuration file, created by # sphinx-quickstart on Wed Nov 18 20:17:31 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) extensions = [ 'djangodocs', 'sphinx.ext.intersphinx' ] # Added manually to reference other sphinx documentations intersphinx_mapping = { 'python': ('http://docs.python.org/2.7', None), 'sphinx': ('http://sphinx.pocoo.org/', None), 'django': ('http://docs.djangoproject.com/en/dev/', 'http://docs.djangoproject.com/en/dev/_objects/'), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'django-dirtyfields' copyright = u'2015, Romain Garrigues' author = u'Romain Garrigues' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.8.1' # The full version, including alpha/beta/rc tags. release = '0.8.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'django-dirtyfieldsdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'django-dirtyfields.tex', u'django-dirtyfields Documentation', u'Romain Garrigues', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'django-dirtyfields', u'django-dirtyfields Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'django-dirtyfields', u'django-dirtyfields Documentation', author, 'django-dirtyfields', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False django-dirtyfields-1.3/docs/contributing.rst000066400000000000000000000002471314730305500213340ustar00rootroot00000000000000Contributing ============ If you're interested in developing it, you can launch project tests on that way: :: $ pip install tox $ pip install -e . $ tox django-dirtyfields-1.3/docs/credits.rst000066400000000000000000000002731314730305500202610ustar00rootroot00000000000000Credits ======= This code has largely be adapted from what was made available at `Stack Overflow`_. .. _Stack Overflow: http://stackoverflow.com/questions/110803/dirty-fields-in-django django-dirtyfields-1.3/docs/index.rst000066400000000000000000000145561314730305500177440ustar00rootroot00000000000000 Welcome to django-dirtyfields's documentation! ============================================== Tracking dirty fields on a Django model instance. Dirty means that field in-memory and database values are different. `Source code `_ Install ======= :: $ pip install django-dirtyfields Usage ===== To use ``django-dirtyfields``, you need to: - Inherit from ``DirtyFieldMixin`` in the Django model you want to track. :: from django.db import models from dirtyfields import DirtyFieldsMixin class TestModel(DirtyFieldsMixin, models.Model): """A simple test model to test dirty fields mixin with""" boolean = models.BooleanField(default=True) characters = models.CharField(blank=True, max_length=80) - Use one of these 2 functions on a model instance to know if this instance is dirty, and get the dirty fields: * is\_dirty() * get\_dirty\_fields() Examples ======== :: >>> from tests.models import TestModel >>> tm = TestModel.objects.create(boolean=True,characters="testing") >>> tm.is_dirty() False >>> tm.get_dirty_fields() {} >>> tm.boolean = False >>> tm.is_dirty() True >>> tm.get_dirty_fields() {'boolean': True} Checking foreign key fields. ---------------------------- By default, dirty functions are not checking foreign keys. If you want to also take these relationships into account, use ``check_relationship`` parameter: :: >>> from tests.models import TestModel >>> tm = TestModel.objects.create(fkey=obj1) >>> tm.is_dirty() False >>> tm.get_dirty_fields() {} >>> tm.fkey = obj2 >>> tm.is_dirty() False >>> tm.is_dirty(check_relationship=True) True >>> tm.get_dirty_fields() {} >>> tm.get_dirty_fields(check_relationship=True) {'fkey': 1} Checking many-to-many fields. ---------------------------- By default, dirty functions are not checking many-to-many fields. They are also a bit special, as a call to `.add()` method is directly saving the related object to the database, thus the instance is never dirty. If you want to check these relations, you should set ``ENABLE_M2M_CHECK`` to ``True`` in your model inheriting from ``DirtyFieldsMixin``, use ``check_m2m`` parameter and provide the values you want to test against: :: class TestM2MModel(DirtyFieldsMixin, models.Model): ENABLE_M2M_CHECK = True m2m_field = models.ManyToManyField(AnotherModel) >>> from tests.models import TestM2MModel >>> tm = TestM2MModel.objects.create() >>> tm2 = TestModel.objects.create() >>> tm.is_dirty() False >>> tm.m2m_field.add(tm2) >>> tm.is_dirty() False >>> tm.get_dirty_fields(check_m2m={'m2m_field': set([tm2.id])}) {} >>> tm.get_dirty_fields(check_m2m={'m2m_field': set(["dummy_value])}) {'m2m_field': set([tm2.id])} This can be useful when validating forms with m2m relations, where you receive some ids and want to know if your object in the database needs to be updated with these form values. **WARNING**: this m2m mode will generate extra queries to get m2m relation values each time you will save your objects. 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. Warning: this ``save_dirty_fields`` method will trigger the same signals as django default ``save`` method. But, in django 1.4.22-, as we are using under the hood an ``update`` method, we need to manually send these signals, so be aware that only ``sender`` and ``instance`` arguments are passed to the signal in that context. :: >>> tm.get_dirty_fields() {'fkey': 1} >>> tm.save_dirty_fields() >>> tm.get_dirty_fields() {} Verbose mode ---------------------------- By default, when you use ``get_dirty_fields`` function, if there are dirty fields, only the old value is returned. You can use ``verbose`` option to have more informations, for now a dict with old and new value: :: >>> tm.get_dirty_fields() {'fkey': 1} >>> tm.get_dirty_fields(verbose=True) {'fkey': {'saved': 1, 'current': 3}} Custom comparison function ---------------------------- By default, ``dirtyfields`` compare the value between the database and the memory on a naive way (``==``). After some issues (with timezones for example), a customisable comparison logic has been added. You can now define how you want to compare 2 values by passing a function on DirtyFieldsMixin: :: from django.db import models from dirtyfields import DirtyFieldsMixin def your_function((new_value, old_value, param1): # Your custom comparison code here return new_value == old_value class TestModel(DirtyFieldsMixin, models.Model): compare_function = (your_function, {'param1': 5}) Have a look at ``dirtyfields.compare`` module to get some examples. Why would you want this? ======================== When using :mod:`django:django.db.models.signals` (:data:`django.db.models.signals.pre_save` especially), it is useful to be able to see what fields have changed or not. A signal could change its behaviour depending on whether a specific field has changed, whereas otherwise, you only could work on the event that the model's :meth:`~django.db.models.Model.save` method had been called. .. include:: contributing.rst .. include:: credits.rst Table of Contents: ================== .. toctree:: :maxdepth: 2 contributing credits Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-dirtyfields-1.3/pytest.ini000066400000000000000000000000461314730305500171710ustar00rootroot00000000000000[pytest] django_find_project = false django-dirtyfields-1.3/requirements.txt000066400000000000000000000000301314730305500204150ustar00rootroot00000000000000Django>=1.8 pytz>=2015.7django-dirtyfields-1.3/setup.py000066400000000000000000000011721314730305500166530ustar00rootroot00000000000000from setuptools import setup, find_packages def listify(filename): return [line for line in open(filename, 'r').read().split('\n') if line] setup( name="django-dirtyfields", version="1.3", url='http://github.com/romgar/django-dirtyfields', license='BSD', description=("Tracking dirty fields on a Django model instance " "(actively maintained)"), long_description=open('README.rst', 'r').read(), author='Romain Garrigues', packages=find_packages('src'), package_dir={'': 'src'}, install_requires=listify('requirements.txt'), classifiers=listify('CLASSIFIERS.txt') ) django-dirtyfields-1.3/src/000077500000000000000000000000001314730305500157275ustar00rootroot00000000000000django-dirtyfields-1.3/src/dirtyfields/000077500000000000000000000000001314730305500202515ustar00rootroot00000000000000django-dirtyfields-1.3/src/dirtyfields/__init__.py000066400000000000000000000001341314730305500223600ustar00rootroot00000000000000from __future__ import absolute_import from dirtyfields.dirtyfields import DirtyFieldsMixin django-dirtyfields-1.3/src/dirtyfields/compare.py000066400000000000000000000043321314730305500222530ustar00rootroot00000000000000import datetime import pytz import warnings from django.utils import timezone def compare_states(new_state, original_state, compare_function): modified_field = {} for key, value in new_state.items(): try: original_value = original_state[key] except KeyError: # In some situation, like deferred fields, it can happen that we try to compare the current # state that has some fields not present in original state because of being initially deferred. # We should not include them in the comparison. continue is_identical = compare_function[0](value, original_value, **compare_function[1]) if is_identical: continue modified_field[key] = {'saved': original_value, 'current': value} return modified_field def raw_compare(new_value, old_value): return new_value == old_value def timezone_support_compare(new_value, old_value, timezone_to_set=pytz.UTC): if not (isinstance(new_value, datetime.datetime) and isinstance(old_value, datetime.datetime)): return raw_compare(new_value, old_value) db_value_is_aware = timezone.is_aware(old_value) in_memory_value_is_aware = timezone.is_aware(new_value) if db_value_is_aware == in_memory_value_is_aware: return raw_compare(new_value, old_value) if db_value_is_aware: # If db value is aware, it means that settings.USE_TZ=True, so we need to convert in-memory one warnings.warn(u"DateTimeField received a naive datetime (%s)" u" while time zone support is active." % new_value, RuntimeWarning) new_value = timezone.make_aware(new_value, timezone_to_set).astimezone(pytz.utc) else: # The db is not timezone aware, but the value we are passing for comparison is aware. warnings.warn(u"Time zone support is not active (settings.USE_TZ=False), " u"and you pass a time zone aware value (%s)" u" Converting database value before comparison." % new_value, RuntimeWarning) old_value = timezone.make_aware(old_value, pytz.utc).astimezone(timezone_to_set) return raw_compare(new_value, old_value) django-dirtyfields-1.3/src/dirtyfields/compat.py000066400000000000000000000011761314730305500221130ustar00rootroot00000000000000import sys import django def get_m2m_with_model(given_model): if django.VERSION < (1, 9): return given_model._meta.get_m2m_with_model() else: return [ (f, f.model if f.model != given_model else None) for f in given_model._meta.get_fields() if f.many_to_many and not f.auto_created ] def is_buffer(value): if sys.version_info < (3, 0, 0): return isinstance(value, buffer) else: return isinstance(value, memoryview) def remote_field(field): if django.VERSION < (1, 9): return field.rel else: return field.remote_field django-dirtyfields-1.3/src/dirtyfields/dirtyfields.py000066400000000000000000000135211314730305500231470ustar00rootroot00000000000000# Adapted from http://stackoverflow.com/questions/110803/dirty-fields-in-django 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_buffer, get_m2m_with_model, remote_field class DirtyFieldsMixin(object): compare_function = (raw_compare, {}) # This mode has been introduced to handle some situations like this one: # 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__, weak=False, dispatch_uid='{name}-DirtyFieldsMixin-sweeper'.format( name=self.__class__.__name__)) if self.ENABLE_M2M_CHECK: self._connect_m2m_relations() reset_state(sender=self.__class__, instance=self) 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, weak=False, dispatch_uid='{name}-DirtyFieldsMixin-sweeper-m2m'.format( name=self.__class__.__name__)) def _as_dict(self, check_relationship, include_primary_key=True): 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 if remote_field(field): if not check_relationship: continue 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 isinstance(field_value, (BaseExpression, Combinable)): continue try: # Store the converted value for fields with conversion field_value = field.to_python(field_value) except ValidationError: # The current value is not valid so we cannot convert it pass if is_buffer(field_value): # psycopg2 returns uncopyable type buffer for bytea field_value = str(field_value) # Explanation of copy usage here : # https://github.com/romgar/django-dirtyfields/commit/efd0286db8b874b5d6bd06c9e903b1a0c9cc6b00 all_field[field.name] = deepcopy(field_value) return all_field def _as_dict_m2m(self): m2m_fields = {} if self.pk: 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: # If the object has not yet been saved in the database, all fields are considered dirty # 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: raise ValueError("You can't check m2m fields if ENABLE_M2M_CHECK is set to False") modified_fields = compare_states(self._as_dict(check_relationship), self._original_state, self.compare_function) if check_m2m: modified_m2m_fields = compare_states(check_m2m, self._original_m2m_state, self.compare_function) modified_fields.update(modified_m2m_fields) if not verbose: # Keeps backward compatibility with previous function return modified_fields = {key: value['saved'] for key, value in modified_fields.items()} return modified_fields def is_dirty(self, check_relationship=False, check_m2m=None): return {} != self.get_dirty_fields(check_relationship=check_relationship, check_m2m=check_m2m) def save_dirty_fields(self): dirty_fields = self.get_dirty_fields(check_relationship=True) self.save(update_fields=dirty_fields.keys()) def reset_state(sender, instance, **kwargs): # original state should hold all possible dirty fields to avoid # getting a `KeyError` when checking if a field is dirty or not update_fields = kwargs.pop('update_fields', {}) new_state = instance._as_dict(check_relationship=True) if update_fields: 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: instance._original_m2m_state = instance._as_dict_m2m() django-dirtyfields-1.3/tests/000077500000000000000000000000001314730305500163025ustar00rootroot00000000000000django-dirtyfields-1.3/tests/__init__.py000066400000000000000000000000001314730305500204010ustar00rootroot00000000000000django-dirtyfields-1.3/tests/compat.py000066400000000000000000000003211314730305500201330ustar00rootroot00000000000000 def get_model_name(klass): if hasattr(klass._meta, 'model_name'): model_name = klass._meta.model_name else: # Django < 1.6 model_name = klass._meta.module_name return model_name django-dirtyfields-1.3/tests/django_settings.py000066400000000000000000000013201314730305500220320ustar00rootroot00000000000000# Minimum files that are needed to run django test suite SECRET_KEY = 'WE DONT CARE ABOUT IT' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 'NAME': 'dirtyfields.db', # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } } INSTALLED_APPS = ('tests', ) django-dirtyfields-1.3/tests/models.py000066400000000000000000000103431314730305500201400ustar00rootroot00000000000000import django from django.db import models from django.db.models.signals import pre_save from django.utils import timezone from dirtyfields import DirtyFieldsMixin from dirtyfields.compare import timezone_support_compare from tests.utils import is_postgresql_env_with_json_field class TestModel(DirtyFieldsMixin, models.Model): """A simple test model to test dirty fields mixin with""" boolean = models.BooleanField(default=True) characters = models.CharField(blank=True, max_length=80) class TestModelWithDecimalField(DirtyFieldsMixin, models.Model): decimal_field = models.DecimalField(decimal_places=2, max_digits=10) class TestModelWithForeignKey(DirtyFieldsMixin, models.Model): fkey = models.ForeignKey(TestModel) class TestMixedFieldsModel(DirtyFieldsMixin, models.Model): fkey = models.ForeignKey(TestModel) characters = models.CharField(blank=True, max_length=80) class TestModelWithOneToOneField(DirtyFieldsMixin, models.Model): o2o = models.OneToOneField(TestModel) class TestModelWithNonEditableFields(DirtyFieldsMixin, models.Model): dt = models.DateTimeField(auto_now_add=True) characters = models.CharField(blank=True, max_length=80, editable=False) boolean = models.BooleanField(default=True) class TestModelWithSelfForeignKey(DirtyFieldsMixin, models.Model): fkey = models.ForeignKey("self", blank=True, null=True) class OrdinaryTestModel(models.Model): boolean = models.BooleanField(default=True) characters = models.CharField(blank=True, max_length=80) class OrdinaryTestModelWithForeignKey(models.Model): fkey = models.ForeignKey(OrdinaryTestModel) class SubclassModel(TestModel): pass class TestExpressionModel(DirtyFieldsMixin, models.Model): counter = models.IntegerField(default=0) class TestDatetimeModel(DirtyFieldsMixin, models.Model): compare_function = (timezone_support_compare, {}) datetime_field = models.DateTimeField(default=timezone.now) class TestCurrentDatetimeModel(DirtyFieldsMixin, models.Model): compare_function = (timezone_support_compare, {'timezone_to_set': timezone.get_current_timezone()}) datetime_field = models.DateTimeField(default=timezone.now) class TestM2MModel(DirtyFieldsMixin, models.Model): m2m_field = models.ManyToManyField(TestModel) ENABLE_M2M_CHECK = True class TestM2MModelWithoutM2MModeEnabled(DirtyFieldsMixin, models.Model): m2m_field = models.ManyToManyField(TestModel) class TestModelWithCustomPK(DirtyFieldsMixin, models.Model): custom_primary_key = models.CharField(max_length=80, primary_key=True) class TestM2MModelWithCustomPKOnM2M(DirtyFieldsMixin, models.Model): m2m_field = models.ManyToManyField(TestModelWithCustomPK) class TestModelWithPreSaveSignal(DirtyFieldsMixin, models.Model): data = models.CharField(max_length=255) data_updated_on_presave = models.CharField(max_length=255, blank=True, null=True) @staticmethod def pre_save(instance, *args, **kwargs): dirty_fields = instance.get_dirty_fields() # only works for case2 if 'data' in dirty_fields: if 'specific_value' in instance.data: instance.data_updated_on_presave = 'presave_value' pre_save.connect(TestModelWithPreSaveSignal.pre_save, sender=TestModelWithPreSaveSignal) class TestModelWithoutM2MCheck(DirtyFieldsMixin, models.Model): characters = models.CharField(blank=True, max_length=80) ENABLE_M2M_CHECK = False class TestDoubleForeignKeyModel(DirtyFieldsMixin, models.Model): fkey1 = models.ForeignKey(TestModel) fkey2 = models.ForeignKey(TestModel, null=True, related_name='fkey2') if is_postgresql_env_with_json_field(): from django.contrib.postgres.fields import JSONField 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'] django-dirtyfields-1.3/tests/postgresql_django_settings.py000066400000000000000000000010171314730305500243200ustar00rootroot00000000000000# Minimum files that are needed to run django test suite SECRET_KEY = 'WE DONT CARE ABOUT IT' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'dirtyfields_test', # Should be the same defined in .travis.yml 'USER': 'postgres', # postgres user is by default created in travis-ci 'PASSWORD': '', # postgres user has no password on travis-ci 'HOST': 'localhost', 'PORT': '5432', # default postgresql port } } INSTALLED_APPS = ('tests', ) django-dirtyfields-1.3/tests/test_core.py000066400000000000000000000105321314730305500206440ustar00rootroot00000000000000from decimal import Decimal import pytest from .models import (TestModel, TestModelWithForeignKey, TestModelWithOneToOneField, SubclassModel, TestModelWithDecimalField) @pytest.mark.django_db def test_is_dirty_function(): tm = TestModel.objects.create() # If the object has just been saved in the db, fields are not dirty assert tm.get_dirty_fields() == {} assert not tm.is_dirty() # As soon as we change a field, it becomes dirty tm.boolean = False assert tm.get_dirty_fields() == {'boolean': True} assert tm.is_dirty() @pytest.mark.django_db def test_dirty_fields(): tm = TestModel() # Initial state is dirty, so should return all fields assert tm.get_dirty_fields() == {'boolean': True, 'characters': ''} tm.save() # Saving them make them not dirty anymore assert tm.get_dirty_fields() == {} # Changing values should flag them as dirty again tm.boolean = False tm.characters = 'testing' assert tm.get_dirty_fields() == { 'boolean': True, 'characters': '' } # Resetting them to original values should unflag tm.boolean = True assert tm.get_dirty_fields() == { 'characters': '' } @pytest.mark.django_db def test_dirty_fields_for_notsaved_pk(): tm = TestModel(id=1) # Initial state is dirty, so should return all fields assert tm.get_dirty_fields() == {'id': 1, 'boolean': True, 'characters': ''} tm.save() # Saving them make them not dirty anymore assert tm.get_dirty_fields() == {} @pytest.mark.django_db def test_relationship_option_for_foreign_key(): tm1 = TestModel.objects.create() tm2 = TestModel.objects.create() tm = TestModelWithForeignKey.objects.create(fkey=tm1) # Let's change the foreign key value and see what happens tm.fkey = tm2 # Default dirty check is not taking foreign keys into account assert tm.get_dirty_fields() == {} # But if we use 'check_relationships' param, then foreign keys are compared assert tm.get_dirty_fields(check_relationship=True) == { 'fkey': tm1.pk } @pytest.mark.django_db def test_relationship_option_for_one_to_one_field(): tm1 = TestModel.objects.create() tm2 = TestModel.objects.create() tm = TestModelWithOneToOneField.objects.create(o2o=tm1) # Let's change the one to one field and see what happens tm.o2o = tm2 # Default dirty check is not taking onetoone fields into account assert tm.get_dirty_fields() == {} # But if we use 'check_relationships' param, then one to one fields are compared assert tm.get_dirty_fields(check_relationship=True) == { 'o2o': tm1.pk } @pytest.mark.django_db def test_non_local_fields(): subclass = SubclassModel.objects.create(characters='foo') subclass.characters = 'spam' assert subclass.get_dirty_fields() == {'characters': 'foo'} @pytest.mark.django_db def test_decimal_field_correctly_managed(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/4 tm = TestModelWithDecimalField.objects.create(decimal_field=Decimal(2.00)) tm.decimal_field = 2.0 assert tm.get_dirty_fields() == {} tm.decimal_field = u"2.00" assert tm.get_dirty_fields() == {} @pytest.mark.django_db def test_deferred_fields(): TestModel.objects.create() qs = TestModel.objects.only('boolean') tm = qs[0] tm.boolean = False assert tm.get_dirty_fields() == {'boolean': True} tm.characters = 'foo' # 'characters' is not tracked as it is deferred assert tm.get_dirty_fields() == {'boolean': True} def test_validationerror(): # Initialize the model with an invalid value tm = TestModel(boolean=None) # Should not raise ValidationError assert tm.get_dirty_fields() == {'boolean': None, 'characters': ''} tm.boolean = False assert tm.get_dirty_fields() == {'boolean': False, 'characters': ''} @pytest.mark.django_db def test_verbose_mode(): tm = TestModel.objects.create() tm.boolean = False 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''} } django-dirtyfields-1.3/tests/test_json_field.py000066400000000000000000000012611314730305500220270ustar00rootroot00000000000000import unittest import pytest from django.db import models from dirtyfields import DirtyFieldsMixin JSON_FIELD_AVAILABLE = False try: from jsonfield import JSONField JSON_FIELD_AVAILABLE = True except ImportError: pass if JSON_FIELD_AVAILABLE: class JSONFieldModel(DirtyFieldsMixin, models.Model): json_field = JSONField() @unittest.skipIf(not JSON_FIELD_AVAILABLE, 'django jsonfield library required') @pytest.mark.django_db def test_json_field(): tm = JSONFieldModel.objects.create(json_field={'data': [1, 2, 3]}) data = tm.json_field['data'] data.append(4) assert tm.get_dirty_fields() == { 'json_field': {'data': [1, 2, 3]} } django-dirtyfields-1.3/tests/test_m2m_fields.py000066400000000000000000000037611314730305500217430ustar00rootroot00000000000000import pytest from .models import TestModel, TestM2MModel, TestModelWithCustomPK, TestM2MModelWithCustomPKOnM2M, \ TestModelWithoutM2MCheck, TestM2MModelWithoutM2MModeEnabled @pytest.mark.django_db def test_dirty_fields_on_m2m(): tm = TestM2MModel.objects.create() tm2 = TestModel.objects.create() tm.m2m_field.add(tm2) assert tm._as_dict_m2m() == {'m2m_field': set([tm2.id])} # m2m check should be explicit: you have to give the values you want to compare with db state. # This first assertion means that m2m_field has one element of id tm2 in the database. assert tm.get_dirty_fields(check_m2m={'m2m_field': set([tm2.id])}) == {} # This second assertion means that I'm expecting a m2m_field that is related to an element with id 0 # As it differs, we return the previous saved elements. assert tm.get_dirty_fields(check_m2m={'m2m_field': set([0])}) == {'m2m_field': set([tm2.id])} assert tm.get_dirty_fields(check_m2m={'m2m_field': set([0, tm2.id])}) == {'m2m_field': set([tm2.id])} @pytest.mark.django_db def test_dirty_fields_on_m2m_not_possible_if_not_enabled(): tm = TestM2MModelWithoutM2MModeEnabled.objects.create() tm2 = TestModel.objects.create() tm.m2m_field.add(tm2) with pytest.raises(ValueError): assert tm.get_dirty_fields(check_m2m={'m2m_field': set([tm2.id])}) == {} @pytest.mark.django_db def test_m2m_check_with_custom_primary_key(): # test for bug: https://github.com/romgar/django-dirtyfields/issues/74 tm = TestModelWithCustomPK.objects.create(custom_primary_key='pk1') m2m_model = TestM2MModelWithCustomPKOnM2M.objects.create() # This line was triggering this error: # AttributeError: 'TestModelWithCustomPK' object has no attribute 'id' m2m_model.m2m_field.add(tm) @pytest.mark.django_db def test_m2m_disabled_does_not_allow_to_check_m2m_fields(): tm = TestModelWithoutM2MCheck.objects.create() with pytest.raises(Exception): assert tm.get_dirty_fields(check_m2m={'dummy': True}) django-dirtyfields-1.3/tests/test_memory_leak.py000066400000000000000000000006121314730305500222160ustar00rootroot00000000000000import 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!' django-dirtyfields-1.3/tests/test_non_regression.py000066400000000000000000000136741314730305500227600ustar00rootroot00000000000000import pytest from django.db import IntegrityError from django.test.utils import override_settings from .models import (TestModel, TestModelWithForeignKey, TestModelWithNonEditableFields, OrdinaryTestModel, OrdinaryTestModelWithForeignKey, TestModelWithSelfForeignKey, TestExpressionModel, TestModelWithPreSaveSignal, TestDoubleForeignKeyModel) from .utils import assert_select_number_queries_on_model @pytest.mark.django_db def test_slicing_and_only(): # test for bug: https://github.com/depop/django-dirtyfields/issues/1 for _ in range(10): TestModelWithNonEditableFields.objects.create() qs_ = TestModelWithNonEditableFields.objects.only('pk').filter() [o for o in qs_.filter().order_by('pk')] @pytest.mark.django_db def test_dirty_fields_ignores_the_editable_property_of_fields(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/17 tm = TestModelWithNonEditableFields.objects.create() # Changing values should flag them as dirty tm.boolean = False tm.characters = 'testing' assert tm.get_dirty_fields() == { 'boolean': True, 'characters': '' } @pytest.mark.django_db def test_mandatory_foreign_key_field_not_initialized_is_not_raising_related_object_exception(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/26 with pytest.raises(IntegrityError): TestModelWithForeignKey.objects.create() @pytest.mark.django_db @override_settings(DEBUG=True) # The test runner sets DEBUG to False. Set to True to enable SQL logging. def test_relationship_model_loading_issue(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/34 # Query tests with models that are not using django-dirtyfields tm1 = OrdinaryTestModel.objects.create() tm2 = OrdinaryTestModel.objects.create() OrdinaryTestModelWithForeignKey.objects.create(fkey=tm1) OrdinaryTestModelWithForeignKey.objects.create(fkey=tm2) with assert_select_number_queries_on_model(OrdinaryTestModelWithForeignKey, 1): with assert_select_number_queries_on_model(OrdinaryTestModel, 0): # should be 0 since we don't access the relationship for now for tmf in OrdinaryTestModelWithForeignKey.objects.all(): tmf.pk with assert_select_number_queries_on_model(OrdinaryTestModelWithForeignKey, 1): with assert_select_number_queries_on_model(OrdinaryTestModel, 2): for tmf in OrdinaryTestModelWithForeignKey.objects.all(): tmf.fkey # access the relationship here with assert_select_number_queries_on_model(OrdinaryTestModelWithForeignKey, 1): with assert_select_number_queries_on_model(OrdinaryTestModel, 0): # should be 0 since we use `select_related` for tmf in OrdinaryTestModelWithForeignKey.objects.select_related('fkey').all(): tmf.fkey # access the relationship here # Query tests with models that are using django-dirtyfields tm1 = TestModel.objects.create() tm2 = TestModel.objects.create() TestModelWithForeignKey.objects.create(fkey=tm1) TestModelWithForeignKey.objects.create(fkey=tm2) with assert_select_number_queries_on_model(TestModelWithForeignKey, 1): with assert_select_number_queries_on_model(TestModel, 0): # should be 0, was 2 before bug fixing for tmf in TestModelWithForeignKey.objects.all(): tmf.pk # we don't need the relationship here with assert_select_number_queries_on_model(TestModelWithForeignKey, 1): with assert_select_number_queries_on_model(TestModel, 2): for tmf in TestModelWithForeignKey.objects.all(): tmf.fkey # access the relationship here with assert_select_number_queries_on_model(TestModelWithForeignKey, 1): with assert_select_number_queries_on_model(TestModel, 0): # should be 0 since we use `selected_related` (was 2 before) for tmf in TestModelWithForeignKey.objects.select_related('fkey').all(): tmf.fkey # access the relationship here @pytest.mark.django_db def test_relationship_option_for_foreign_key_to_self(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/22 tm = TestModelWithSelfForeignKey.objects.create() tm1 = TestModelWithSelfForeignKey.objects.create(fkey=tm) tm.fkey = tm1 tm.save() # Trying to access an instance was triggering a "RuntimeError: maximum recursion depth exceeded" TestModelWithSelfForeignKey.objects.all()[0] @pytest.mark.django_db def test_expressions_not_taken_into_account_for_dirty_check(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/39 from django.db.models import F tm = TestExpressionModel.objects.create() tm.counter = F('counter') + 1 # This save() was raising a ValidationError: [u"'F(counter) + Value(1)' value must be an integer."] # caused by a call to_python() on an expression node tm.save() @pytest.mark.django_db def test_pre_save_signal_make_dirty_checking_not_consistent(): # first case model = TestModelWithPreSaveSignal.objects.create(data='specific_value') assert model.data_updated_on_presave is 'presave_value' # second case model = TestModelWithPreSaveSignal(data='specific_value') model.save() assert model.data_updated_on_presave is 'presave_value' # third case model = TestModelWithPreSaveSignal() model.data = 'specific_value' model.save() assert model.data_updated_on_presave is 'presave_value' @pytest.mark.django_db def test_foreign_key_deferred_field(): # Non regression test case for bug: # https://github.com/romgar/django-dirtyfields/issues/84 tm = TestModel.objects.create() TestDoubleForeignKeyModel.objects.create(fkey1=tm) list(TestDoubleForeignKeyModel.objects.only('fkey1')) # RuntimeError was raised here! django-dirtyfields-1.3/tests/test_postgresql_specific.py000066400000000000000000000011521314730305500237620ustar00rootroot00000000000000import pytest from tests.utils import is_postgresql_env_with_json_field @pytest.mark.skipif(not is_postgresql_env_with_json_field(), reason="requires postgresql and Django 1.9+") @pytest.mark.django_db def test_dirty_json_field(): from tests.models import TestModelWithJSONField tm = TestModelWithJSONField.objects.create(json_field={'data': [1, 2, 3]}) data = tm.json_field['data'] data.append(4) assert tm.get_dirty_fields(verbose=True) == { 'json_field': { 'current': {'data': [1, 2, 3, 4]}, 'saved': {'data': [1, 2, 3]} } } django-dirtyfields-1.3/tests/test_save_fields.py000066400000000000000000000071221314730305500222010ustar00rootroot00000000000000import unittest import django import pytest from .models import TestModel, TestMixedFieldsModel, TestModelWithForeignKey from .utils import assert_number_of_queries_on_regex @pytest.mark.django_db def test_save_dirty_simple_field(): tm = TestModel.objects.create() tm.characters = 'new_character' with assert_number_of_queries_on_regex(r'.*characters.*', 1): with assert_number_of_queries_on_regex(r'.*boolean.*', 1): tm.save() tm.characters = 'new_character_2' # Naive checking on fields involved in Django query # boolean unchanged field is not updated on Django update query: GOOD ! with assert_number_of_queries_on_regex(r'.*characters.*', 1): with assert_number_of_queries_on_regex(r'.*boolean.*', 0): tm.save_dirty_fields() # We also check that the value has been correctly updated by our custom function assert tm.get_dirty_fields() == {} assert TestModel.objects.get(pk=tm.pk).characters == 'new_character_2' @pytest.mark.django_db def test_save_dirty_related_field(): tm1 = TestModel.objects.create() tm2 = TestModel.objects.create() tmfm = TestMixedFieldsModel.objects.create(fkey=tm1) tmfm.fkey = tm2 with assert_number_of_queries_on_regex(r'.*fkey_id.*', 1): with assert_number_of_queries_on_regex(r'.*characters.*', 1): tmfm.save() tmfm.fkey = tm1 # Naive checking on fields involved in Django query # characters unchanged field is not updated on Django update query: GOOD ! with assert_number_of_queries_on_regex(r'.*fkey_id.*', 1): with assert_number_of_queries_on_regex(r'.*characters.*', 0): tmfm.save_dirty_fields() # We also check that the value has been correctly updated by our custom function assert tmfm.get_dirty_fields() == {} assert TestMixedFieldsModel.objects.get(pk=tmfm.pk).fkey_id == tm1.id @pytest.mark.django_db def test_save_only_specific_fields_should_let_other_fields_dirty(): tm = TestModel.objects.create(boolean=True, characters='dummy') tm.boolean = False tm.characters = 'new_dummy' tm.save(update_fields=['boolean']) # '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']) django-dirtyfields-1.3/tests/test_specified_fields.py000066400000000000000000000014751314730305500232030ustar00rootroot00000000000000import 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([])}) == {} django-dirtyfields-1.3/tests/test_timezone_aware_fields.py000066400000000000000000000041531314730305500242550ustar00rootroot00000000000000import pytest import pytz from datetime import datetime from django.test.utils import override_settings from .models import TestDatetimeModel, TestCurrentDatetimeModel @override_settings(USE_TZ=True, TIME_ZONE='America/Chicago') @pytest.mark.django_db def test_datetime_fields_when_aware_db_and_naive_current_value(): tm = TestDatetimeModel.objects.create(datetime_field=datetime(2000, 1, 1, tzinfo=pytz.utc)) # Adding a naive datetime tm.datetime_field = datetime(2016, 1, 1) assert tm.get_dirty_fields() == {'datetime_field': datetime(2000, 1, 1, tzinfo=pytz.utc)} @override_settings(USE_TZ=False) @pytest.mark.django_db def test_datetime_fields_when_naive_db_and_aware_current_value(): tm = TestDatetimeModel.objects.create(datetime_field=datetime(2000, 1, 1)) # Adding an aware datetime tm.datetime_field = datetime(2016, 1, 1, tzinfo=pytz.utc) assert tm.get_dirty_fields() == {'datetime_field': datetime(2000, 1, 1)} @override_settings(USE_TZ=True, TIME_ZONE='America/Chicago') @pytest.mark.django_db def test_datetime_fields_with_current_timezone_conversion(): tm = TestCurrentDatetimeModel.objects.create(datetime_field=datetime(2000, 1, 1, 12, 0, 0, tzinfo=pytz.utc)) # Adding a naive datetime, that will be converted to local timezone. tm.datetime_field = datetime(2000, 1, 1, 6, 0, 0) # Chicago is UTC-6h, this field shouldn't be dirty, as we will automatically set this naive datetime # with current timezone and then convert it to utc to compare it with database one. assert tm.get_dirty_fields() == {} @override_settings(USE_TZ=False, TIME_ZONE='America/Chicago') @pytest.mark.django_db def test_datetime_fields_with_current_timezone_conversion_without_timezone_support(): tm = TestCurrentDatetimeModel.objects.create(datetime_field=datetime(2000, 1, 1, 12, 0, 0)) # Adding an aware datetime chicago_timezone = pytz.timezone('America/Chicago') tm.datetime_field = chicago_timezone.localize(datetime(2000, 1, 1, 6, 0, 0), is_dst=None) # If the database is naive, then we consider that it is defined as in UTC. assert tm.get_dirty_fields() == {} django-dirtyfields-1.3/tests/utils.py000066400000000000000000000034711314730305500200210ustar00rootroot00000000000000import re import django from django.conf import settings from django.db import connection from .compat import get_model_name class assert_number_queries(object): def __init__(self, number): self.number = number def matched_queries(self): return connection.queries def query_count(self): return len(self.matched_queries()) def __enter__(self): self.DEBUG = settings.DEBUG settings.DEBUG = True self.num_queries_before = self.query_count() def __exit__(self, type, value, traceback): self.num_queries_after = self.query_count() assert self.num_queries_after - self.num_queries_before == self.number settings.DEBUG = self.DEBUG class RegexMixin(object): regex = None def matched_queries(self): matched_queries = super(RegexMixin, self).matched_queries() if self.regex is not None: pattern = re.compile(self.regex) regex_compliant_queries = [query for query in matched_queries if pattern.match(query.get('sql'))] return regex_compliant_queries class assert_number_of_queries_on_regex(RegexMixin, assert_number_queries): def __init__(self, regex, number): super(assert_number_of_queries_on_regex, self).__init__(number) self.regex = regex class assert_select_number_queries_on_model(assert_number_of_queries_on_regex): def __init__(self, model_class, number): model_name = get_model_name(model_class) regex = r'^.*SELECT.*FROM "tests_%s".*$' % model_name super(assert_select_number_queries_on_model, self).__init__(regex, number) def is_postgresql_env_with_json_field(): try: PG_VERSION = connection.pg_version except AttributeError: PG_VERSION = 0 return PG_VERSION >= 90400 and django.VERSION >= (1, 9) django-dirtyfields-1.3/tox.ini000066400000000000000000000015711314730305500164570ustar00rootroot00000000000000[tox] envlist = django18,django19,django110,django111,coverage,postgresql [testenv] setenv = PYTHONPATH = {toxinidir} commands = py.test --ds=tests.django_settings -v deps = pytest==3.0.6 pytest-django==3.1.2 jsonfield==1.0.3 pytz [testenv:django18] deps = django>=1.8,<1.8.99 {[testenv]deps} [testenv:django19] deps = django>=1.9,<1.9.99 {[testenv]deps} [testenv:django110] 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.11,<1.11.99 psycopg2 {[testenv]deps} [testenv:coverage] commands = coverage run --source dirtyfields -m py.test --ds=tests.django_settings deps = coverage {[testenv:django110]deps}