django-model-utils (2.5.2-2) unstable; urgency=medium
[ Ben Finney ] * Improve package description. Closes: #834116, Closes: #834117. [ Brian May ] * New upstream release. * Fix FTBFS: Closes: #828651. * Update standards version to 3.9.8. # imported from the archive
This commit is contained in:
commit
5195610be0
|
@ -0,0 +1,4 @@
|
|||
[run]
|
||||
source = model_utils
|
||||
omit = model_utils/tests/*
|
||||
branch = 1
|
|
@ -0,0 +1,17 @@
|
|||
# http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{py,rst,ini}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,11 @@
|
|||
dist/*
|
||||
django_model_utils.egg-info/*
|
||||
HGREV
|
||||
.coverage
|
||||
.tox/
|
||||
Django-*.egg
|
||||
*.pyc
|
||||
htmlcov/
|
||||
docs/_build/
|
||||
.idea/
|
||||
.eggs/
|
|
@ -0,0 +1,7 @@
|
|||
^dist/
|
||||
^django_model_utils\.egg-info/
|
||||
^HGREV$
|
||||
^\.coverage$
|
||||
^\.tox/
|
||||
^Django.*\.egg$
|
||||
^htmlcov/
|
|
@ -0,0 +1,6 @@
|
|||
b5efc435bb7e21b0d7ba422d28d174ccca3b3322 0.2.0
|
||||
71b54b8b44fa2456beebd51c474ba55ad625486a 0.3.1
|
||||
1e6f730f8c3a648c9fb70844a68fcfa663608600 0.4.0
|
||||
004dbee634cb661c52acac034063989e521c4bb8 0.5.0
|
||||
bd164041e5fabd64de19c38fefe9af9237a2a59e 1.0.0
|
||||
92792fb14a51b580e5cc8991e815f3b3b57a6204 1.1.0
|
|
@ -0,0 +1,47 @@
|
|||
language: python
|
||||
|
||||
python: 2.7
|
||||
|
||||
env:
|
||||
- TOXENV=py26-django14
|
||||
- TOXENV=py26-django15
|
||||
- TOXENV=py26-django16
|
||||
- TOXENV=py27-django110
|
||||
- TOXENV=py27-django14
|
||||
- TOXENV=py27-django15
|
||||
- TOXENV=py27-django15_nosouth
|
||||
- TOXENV=py27-django16
|
||||
- TOXENV=py27-django17
|
||||
- TOXENV=py27-django18
|
||||
- TOXENV=py27-django19
|
||||
- TOXENV=py27-django_trunk
|
||||
- TOXENV=py33-django15
|
||||
- TOXENV=py33-django16
|
||||
- TOXENV=py33-django17
|
||||
- TOXENV=py33-django18
|
||||
- TOXENV=py34-django110
|
||||
- TOXENV=py34-django17
|
||||
- TOXENV=py34-django18
|
||||
- TOXENV=py34-django19
|
||||
- TOXENV=py34-django_trunk
|
||||
- TOXENV=py35-django110
|
||||
- TOXENV=py35-django18
|
||||
- TOXENV=py35-django19
|
||||
- TOXENV=py35-django_trunk
|
||||
|
||||
install:
|
||||
- pip install --upgrade pip setuptools tox virtualenv coveralls
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: TOXENV=py27-django110
|
||||
- env: TOXENV=py34-django110
|
||||
- env: TOXENV=py35-django110
|
||||
- env: TOXENV=py27-django_trunk
|
||||
- env: TOXENV=py34-django_trunk
|
||||
- env: TOXENV=py35-django_trunk
|
||||
|
||||
after_success: coveralls
|
|
@ -0,0 +1,41 @@
|
|||
ad-m <github.com/ad-m>
|
||||
Alejandro Varas <alej0varas@gmail.com>
|
||||
Alex Orange <crazycasta@gmail.com>
|
||||
Andy Freeland <andy@andyfreeland.net>
|
||||
Artis Avotins <artis.avotins@gmail.com>
|
||||
Bram Boogaard <b.boogaard@auto-interactive.nl>
|
||||
Carl Meyer <carl@dirtcircle.com>
|
||||
Curtis Maloney <curtis@tinbrain.net>
|
||||
Den Lesnov
|
||||
Dmytro Kyrychuk <dmytro.kyrychuck@gmail.com>
|
||||
Donald Stufft <donald.stufft@gmail.com>
|
||||
Douglas Meehan <dmeehan@gmail.com>
|
||||
Facundo Gaich <facugaich@gmail.com>
|
||||
Felipe Prenholato <philipe.rp@gmail.com>
|
||||
Filipe Ximenes <filipeximenes@gmail.com>
|
||||
Gregor Müllegger <gregor@muellegger.de>
|
||||
ivirabyan
|
||||
James Oakley <jfunk@funktronics.ca>
|
||||
Jannis Leidel <jannis@leidel.info>
|
||||
Jarek Glowacki <github.com/jarekwg>
|
||||
Javier García Sogo <jgsogo@gmail.com>
|
||||
Jeff Elmore <jeffelmore.org>
|
||||
Keryn Knight <kerynknight.com>
|
||||
Matthew Schinckel <matt@schinckel.net>
|
||||
Michael van Tellingen <michaelvantellingen@gmail.com>
|
||||
Mike Bryant <mike@mikebryant.me.uk>
|
||||
Mikhail Silonov <silonov.pro>
|
||||
Patryk Zawadzki <patrys@room-303.com>
|
||||
Paul McLanahan <paul@mclanahan.net>
|
||||
Philipp Steinhardt <steinhardt@myvision.de>
|
||||
Rinat Shigapov <rinatshigapov@gmail.com>
|
||||
Rodney Folz <rodney@rodneyfolz.com>
|
||||
rsenkbeil <github.com/rsenkbeil>
|
||||
Ryan Kaskel <dev@ryankaskel.com>
|
||||
Simon Meers <simon@simonmeers.com>
|
||||
sayane
|
||||
Tony Aldridge <zaragopha@hotmail.com>
|
||||
Travis Swicegood <travis@domain51.com>
|
||||
Trey Hunner <trey@treyhunner.com>
|
||||
Karl Wan Nan Wo <karl.wnw@gmail.com>
|
||||
zyegfryed
|
|
@ -0,0 +1,335 @@
|
|||
CHANGES
|
||||
=======
|
||||
|
||||
2.5.2 (2016.08.09)
|
||||
------------------
|
||||
|
||||
* Include `runtests.py` in sdist.
|
||||
|
||||
|
||||
2.5.1 (2016.08.03)
|
||||
------------------
|
||||
|
||||
* Fix `InheritanceQuerySet` raising an `AttributeError` exception
|
||||
under Django 1.9.
|
||||
|
||||
* Django 1.10 support regressed with changes between pre-alpha and final
|
||||
release; 1.10 currently not supported.
|
||||
|
||||
|
||||
2.5 (2016.04.18)
|
||||
----------------
|
||||
|
||||
* Drop support for Python 3.2.
|
||||
|
||||
* Add support for Django 1.10 pre-alpha.
|
||||
|
||||
* Track foreign keys on parent models properly when a tracker
|
||||
is defined on a child model. Fixes GH-214.
|
||||
|
||||
|
||||
2.4 (2015.12.03)
|
||||
----------------
|
||||
|
||||
* Remove `PassThroughManager`. Use Django's built-in `QuerySet.as_manager()`
|
||||
and/or `Manager.from_queryset()` utilities instead.
|
||||
|
||||
* Add support for Django 1.9.
|
||||
|
||||
|
||||
2.3.1 (2015-07-20)
|
||||
------------------
|
||||
|
||||
* Remove all translation-related automation in `setup.py`. Fixes GH-178 and
|
||||
GH-179. Thanks Joe Weiss, Matt Molyneaux, and others for the reports.
|
||||
|
||||
|
||||
2.3 (2015.07.17)
|
||||
----------------
|
||||
|
||||
* Keep track of deferred fields on model instance instead of on
|
||||
FieldInstanceTracker instance. Fixes accessing deferred fields for multiple
|
||||
instances of a model from the same queryset. Thanks Bram Boogaard. Merge of
|
||||
GH-151.
|
||||
|
||||
* Fix Django 1.7 migrations compatibility for SplitField. Thanks ad-m. Merge of
|
||||
GH-157; fixes GH-156.
|
||||
|
||||
* Add German translations.
|
||||
|
||||
* Django 1.8 compatibility.
|
||||
|
||||
|
||||
2.2 (2014.07.31)
|
||||
----------------
|
||||
|
||||
* Revert GH-130, restoring ability to access ``FieldTracker`` changes in
|
||||
overridden ``save`` methods or ``post_save`` handlers. This reopens GH-83
|
||||
(inability to pickle models with ``FieldTracker``) until a solution can be
|
||||
found that doesn't break behavior otherwise. Thanks Brian May for the
|
||||
report. Fixes GH-143.
|
||||
|
||||
|
||||
2.1.1 (2014.07.28)
|
||||
------------------
|
||||
|
||||
* ASCII-fold all non-ASCII characters in changelog; again. Argh. Apologies to
|
||||
those whose names are mangled by this change. It seems that distutils makes
|
||||
it impossible to handle non-ASCII content reliably under Python 3 in a
|
||||
setup.py long_description, when the system encoding may be ASCII. Thanks
|
||||
Brian May for the report. Fixes GH-141.
|
||||
|
||||
|
||||
2.1.0 (2014.07.25)
|
||||
------------------
|
||||
|
||||
* Add support for Django's built-in migrations to ``MonitorField`` and
|
||||
``StatusField``.
|
||||
|
||||
* ``PassThroughManager`` now has support for seeing exposed methods via
|
||||
``dir``, allowing `IPython`_ tab completion to be useful. Merge of GH-104,
|
||||
fixes GH-55.
|
||||
|
||||
* Add pickle support for models using ``FieldTracker``. Thanks Ondrej Slintak
|
||||
for the report. Thanks Matthew Schinckel for the fix. Merge of GH-130,
|
||||
fixes GH-83.
|
||||
|
||||
.. _IPython: http://ipython.org/
|
||||
|
||||
|
||||
2.0.3 (2014.03.19)
|
||||
-------------------
|
||||
|
||||
* Fix ``get_query_set`` vs ``get_queryset`` in ``PassThroughManager`` for
|
||||
Django <1.6. Fixes issues with related managers not filtering by relation
|
||||
properly. Thanks whop, Bojan Mihelac, Daniel Shapiro, and Matthew Schinckel
|
||||
for the report; Matthew for the fix. Merge of GH-121.
|
||||
|
||||
* Fix ``FieldTracker`` with deferred model attributes. Thanks Michael van
|
||||
Tellingen. Merge of GH-115.
|
||||
|
||||
* Fix ``InheritanceManager`` with self-referential FK; avoid infinite
|
||||
recursion. Thanks rsenkbeil. Merge of GH-114.
|
||||
|
||||
2.0.2 (2014.02.19)
|
||||
-------------------
|
||||
|
||||
* ASCII-fold all non-ASCII characters in changelog. Apologies to those whose
|
||||
names are mangled by this change. It seems that distutils makes it impossible
|
||||
to handle non-ASCII content reliably under Python 3 in a setup.py
|
||||
long_description, when the system encoding may be ASCII. Thanks Simone Dalla
|
||||
for the report. Fixes GH-113.
|
||||
|
||||
|
||||
2.0.1 (2014.02.11)
|
||||
-------------------
|
||||
|
||||
* Fix dependency to be on "Django" rather than "django", which plays better
|
||||
with static PyPI mirrors. Thanks Travis Swicegood.
|
||||
|
||||
* Fix issue with attempt to access ``__slots__`` when copying
|
||||
``PassThroughManager``. Thanks Patryk Zawadzki. Merge of GH-105.
|
||||
|
||||
* Improve ``InheritanceManager`` so any attributes added by using extra(select)
|
||||
will be propagated onto children. Thanks Curtis Maloney. Merge of GH-101,
|
||||
fixes GH-34.
|
||||
|
||||
* Added ``InheritanceManagerMixin``, ``InheritanceQuerySetMixin``,
|
||||
``PassThroughManagerMixin``, and ``QueryManagerMixin`` to allow composing
|
||||
their functionality with other custom manager/queryset subclasses (e.g. those
|
||||
in GeoDjango). Thanks Douglas Meehan!
|
||||
|
||||
|
||||
2.0 (2014.01.06)
|
||||
----------------
|
||||
|
||||
* BACKWARDS-INCOMPATIBLE: Indexing into a ``Choices`` instance now translates
|
||||
database representations to human-readable choice names, rather than simply
|
||||
indexing into an array of choice tuples. (Indexing into ``Choices`` was
|
||||
previously not documented.) If you have code that is relying on indexing or
|
||||
slicing ``Choices``, the simplest workaround is to change e.g. ``STATUS[1:]``
|
||||
to ``list(STATUS)[1:]``.
|
||||
|
||||
* Fixed bug with checking for field name conflicts for added query managers on
|
||||
`StatusModel`.
|
||||
|
||||
* Can pass `choices_name` to `StatusField` to use a different name for
|
||||
choices class attribute. ``STATUS`` is used by default.
|
||||
|
||||
* Can pass model subclasses, rather than strings, into
|
||||
`select_subclasses()`. Thanks Keryn Knight. Merge of GH-79.
|
||||
|
||||
* Deepcopying a `Choices` instance no longer fails with infinite recursion in
|
||||
`getattr`. Thanks Leden. Merge of GH-75.
|
||||
|
||||
* `get_subclass()` method is now available on both managers and
|
||||
querysets. Thanks Travis Swicegood. Merge of GH-82.
|
||||
|
||||
* Fix bug in `InheritanceManager` with grandchild classes on Django 1.6+;
|
||||
`select_subclasses('child', 'child__grandchild')` would only ever get to the
|
||||
child class. Thanks Keryn Knight for report and proposed fix.
|
||||
|
||||
* MonitorField now accepts a 'when' parameter. It will update only when the field
|
||||
changes to one of the values specified.
|
||||
|
||||
|
||||
1.5.0 (2013.08.29)
|
||||
------------------
|
||||
|
||||
* `Choices` now accepts option-groupings. Fixes GH-14.
|
||||
|
||||
* `Choices` can now be added to other `Choices` or to any iterable, and can be
|
||||
compared for equality with itself. Thanks Tony Aldridge. (Merge of GH-76.)
|
||||
|
||||
* `Choices` now `__contains__` its Python identifier values. Thanks Keryn
|
||||
Knight. (Merge of GH-69).
|
||||
|
||||
* Fixed a bug causing ``KeyError`` when saving with the parameter
|
||||
``update_fields`` in which there are untracked fields. Thanks Mikhail
|
||||
Silonov. (Merge of GH-70, fixes GH-71).
|
||||
|
||||
* Fixed ``FieldTracker`` usage on inherited models. Fixes GH-57.
|
||||
|
||||
* Added mutable field support to ``FieldTracker`` (Merge of GH-73, fixes GH-74)
|
||||
|
||||
|
||||
1.4.0 (2013.06.03)
|
||||
------------------
|
||||
|
||||
- Introduced ``FieldTracker`` as replacement for ``ModelTracker``, which is now
|
||||
deprecated.
|
||||
|
||||
- ``PassThroughManager.for_queryset_class()`` no longer ignores superclass
|
||||
``get_query_set``. Thanks Andy Freeland.
|
||||
|
||||
- Fixed ``InheritanceManager`` bug with grandchildren in Django 1.6. Thanks
|
||||
CrazyCasta.
|
||||
|
||||
- Fixed lack of ``get_FOO_display`` method for ``StatusField``. Fixes GH-41.
|
||||
|
||||
|
||||
1.3.1 (2013.04.11)
|
||||
------------------
|
||||
|
||||
- Added explicit default to ``BooleanField`` in tests, for Django trunk
|
||||
compatibility.
|
||||
|
||||
- Fixed intermittent ``StatusField`` bug. Fixes GH-29.
|
||||
|
||||
- Added Python 3 support.
|
||||
|
||||
- Dropped support for Django 1.2 and 1.3. Django 1.4.2+ required.
|
||||
|
||||
|
||||
1.3.0 (2013.03.27)
|
||||
------------------
|
||||
|
||||
- Allow specifying default value for a ``StatusField``. Thanks Felipe
|
||||
Prenholato.
|
||||
|
||||
- Fix calling ``create()`` on a ``RelatedManager`` that subclasses a dynamic
|
||||
``PassThroughManager``. Thanks SeiryuZ for the report. Fixes GH-24.
|
||||
|
||||
- Add workaround for https://code.djangoproject.com/ticket/16855 in
|
||||
InheritanceQuerySet to avoid overriding prior calls to
|
||||
``select_related()``. Thanks ivirabyan.
|
||||
|
||||
- Added support for arbitrary levels of model inheritance in
|
||||
InheritanceManager. Thanks ivirabyan. (This feature only works in Django
|
||||
1.6+ due to https://code.djangoproject.com/ticket/16572).
|
||||
|
||||
- Added ``ModelTracker`` for tracking field changes between model saves. Thanks
|
||||
Trey Hunner.
|
||||
|
||||
|
||||
1.2.0 (2013.01.27)
|
||||
------------------
|
||||
|
||||
- Moved primary development from `Bitbucket`_ to `GitHub`_. Bitbucket mirror
|
||||
will continue to receive updates; Bitbucket issue tracker will be closed once
|
||||
all issues tracked in it are resolved.
|
||||
|
||||
.. _BitBucket: https://bitbucket.org/carljm/django-model-utils/overview
|
||||
.. _GitHub: https://github.com/carljm/django-model-utils/
|
||||
|
||||
- Removed deprecated ``ChoiceEnum``, ``InheritanceCastModel``,
|
||||
``InheritanceCastManager``, and ``manager_from``.
|
||||
|
||||
- Fixed pickling of ``PassThroughManager``. Thanks Rinat Shigapov.
|
||||
|
||||
- Set ``use_for_related_fields = True`` on ``QueryManager``.
|
||||
|
||||
- Added ``__len__`` method to ``Choices``. Thanks Ryan Kaskel and James Oakley.
|
||||
|
||||
- Fixed ``InheritanceQuerySet`` on Django 1.5. Thanks Javier Garcia Sogo.
|
||||
|
||||
1.1.0 (2012.04.13)
|
||||
------------------
|
||||
|
||||
- Updated AutoCreatedField, AutoLastModifiedField, MonitorField, and
|
||||
TimeFramedModel to use ``django.utils.timezone.now`` on Django 1.4.
|
||||
Thanks Donald Stufft.
|
||||
|
||||
- Fixed annotation of InheritanceQuerysets. Thanks Jeff Elmore and Facundo
|
||||
Gaich.
|
||||
|
||||
- Dropped support for Python 2.5 and Django 1.1. Both are no longer supported
|
||||
even for security fixes, and should not be used.
|
||||
|
||||
- Added ``PassThroughManager.for_queryset_class()``, which fixes use of
|
||||
``PassThroughManager`` with related fields. Thanks Ryan Kaskel for report and
|
||||
fix.
|
||||
|
||||
- Added ``InheritanceManager.get_subclass()``. Thanks smacker.
|
||||
|
||||
1.0.0 (2011.06.16)
|
||||
------------------
|
||||
|
||||
- Fixed using SplitField on an abstract base model.
|
||||
|
||||
- Fixed issue #8, adding ``use_for_related_fields = True`` to
|
||||
``InheritanceManager``.
|
||||
|
||||
- Added ``PassThroughManager``. Thanks Paul McLanahan.
|
||||
|
||||
- Added pending-deprecation warnings for ``InheritanceCastModel``,
|
||||
``manager_from``, and Django 1.1 support. Removed documentation for the
|
||||
deprecated utilities. Bumped ``ChoiceEnum`` from pending-deprecation to
|
||||
deprecation.
|
||||
|
||||
- Fixed issue #6, bug with InheritanceManager and descriptor fields (e.g.
|
||||
FileField). Thanks zyegfryed for the fix and sayane for tests.
|
||||
|
||||
0.6.0 (2011.02.18)
|
||||
------------------
|
||||
|
||||
- updated SplitField to define get_prep_value rather than get_db_prep_value.
|
||||
This avoids deprecation warnings on Django trunk/1.3, but makes SplitField
|
||||
incompatible with Django versions prior to 1.2.
|
||||
|
||||
- added InheritanceManager, a better approach to selecting subclass instances
|
||||
for Django 1.2+. Thanks Jeff Elmore.
|
||||
|
||||
- added InheritanceCastManager and InheritanceCastQuerySet, to allow bulk
|
||||
casting of a queryset to child types. Thanks Gregor Muellegger.
|
||||
|
||||
0.5.0 (2010.09.24)
|
||||
------------------
|
||||
|
||||
- added manager_from (thanks George Sakkis)
|
||||
- added StatusField, MonitorField, TimeFramedModel, and StatusModel
|
||||
(thanks Jannis Leidel)
|
||||
- deprecated ChoiceEnum and replaced with Choices
|
||||
|
||||
0.4.0 (2010.03.16)
|
||||
------------------
|
||||
|
||||
- added SplitField
|
||||
- added ChoiceEnum
|
||||
- added South support for custom model fields
|
||||
|
||||
0.3.0
|
||||
-----
|
||||
|
||||
* Added ``QueryManager``
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
Contributing
|
||||
============
|
||||
|
||||
Below is a list of tips for submitting issues and pull requests.
|
||||
|
||||
Submitting Issues
|
||||
-----------------
|
||||
|
||||
Issues are easier to reproduce/resolve when they have:
|
||||
|
||||
- A pull request with a failing test demonstrating the issue
|
||||
- A code example that produces the issue consistently
|
||||
- A traceback (when applicable)
|
||||
|
||||
|
||||
Pull Requests
|
||||
-------------
|
||||
|
||||
When creating a pull request:
|
||||
|
||||
- Write tests
|
||||
- Note user-facing changes in the `CHANGES`_ file
|
||||
- Update the documentation
|
||||
- Add yourself to the `AUTHORS`_ file
|
||||
- If you have added or changed translated strings, run ``make messages`` to
|
||||
update the ``.po`` translation files, and update translations for any
|
||||
languages you know. Then run ``make compilemessages`` to compile the ``.mo``
|
||||
files. If your pull request leaves some translations incomplete, please
|
||||
mention that in the pull request and commit message.
|
||||
|
||||
.. _AUTHORS: AUTHORS.rst
|
||||
.. _CHANGES: CHANGES.rst
|
||||
|
||||
|
||||
Translations
|
||||
------------
|
||||
|
||||
If you are able to provide translations for a new language or to update an
|
||||
existing translation file, make sure to run makemessages beforehand::
|
||||
|
||||
python django-admin.py makemessages -l ISO_LANGUAGE_CODE
|
||||
|
||||
This command will collect all translation strings from the source directory
|
||||
and create or update the translation file for the given language. Now open the
|
||||
translation file (.po) with a text-editor and start editing.
|
||||
After you finished editing add yourself to the list of translators.
|
||||
If you have created a new translation, make sure to copy the header from one
|
||||
of the existing translation files.
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
Please add tests for your code and ensure existing tests don't break. To run
|
||||
the tests against your code::
|
||||
|
||||
python setup.py test
|
||||
|
||||
Please use tox to test the code against supported Python and Django versions.
|
||||
First install tox::
|
||||
|
||||
pip install tox coverage
|
||||
|
||||
To run tox and generate a coverage report (in ``htmlcov`` directory)::
|
||||
|
||||
make test
|
||||
|
||||
**Please note**: Before a pull request can be merged, all tests must pass and
|
||||
code/branch coverage in tests must be 100%.
|
|
@ -0,0 +1,28 @@
|
|||
Copyright (c) 2009-2015, Carl Meyer and 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:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* 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.
|
||||
* Neither the name of the author nor the names of other
|
||||
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
|
||||
OWNER 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.
|
|
@ -0,0 +1,8 @@
|
|||
include AUTHORS.rst
|
||||
include CHANGES.rst
|
||||
include LICENSE.txt
|
||||
include MANIFEST.in
|
||||
include README.rst
|
||||
include TODO.rst
|
||||
recursive-include locale django.po
|
||||
include runtests.py
|
|
@ -0,0 +1,21 @@
|
|||
all: init docs test
|
||||
|
||||
init:
|
||||
python setup.py develop
|
||||
pip install tox coverage Sphinx
|
||||
|
||||
test:
|
||||
coverage erase
|
||||
tox
|
||||
coverage html
|
||||
|
||||
docs: documentation
|
||||
|
||||
documentation:
|
||||
python setup.py build_sphinx
|
||||
|
||||
messages:
|
||||
python translations.py make
|
||||
|
||||
compilemessages:
|
||||
python translations.py compile
|
|
@ -0,0 +1,39 @@
|
|||
==================
|
||||
django-model-utils
|
||||
==================
|
||||
|
||||
.. image:: https://secure.travis-ci.org/carljm/django-model-utils.png?branch=master
|
||||
:target: http://travis-ci.org/carljm/django-model-utils
|
||||
.. image:: https://coveralls.io/repos/carljm/django-model-utils/badge.png?branch=master
|
||||
:target: https://coveralls.io/r/carljm/django-model-utils
|
||||
.. image:: https://img.shields.io/pypi/v/django-model-utils.svg
|
||||
:target: https://crate.io/packages/django-model-utils
|
||||
|
||||
Django model mixins and utilities.
|
||||
|
||||
``django-model-utils`` supports `Django`_ 1.4 through 1.9 (latest bugfix
|
||||
release in each series only) on Python 2.6 (through Django 1.6 only), 2.7, 3.3
|
||||
(through Django 1.8 only), 3.4 and 3.5.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
|
||||
This app is available on `PyPI`_.
|
||||
|
||||
.. _PyPI: https://pypi.python.org/pypi/django-model-utils/
|
||||
|
||||
|
||||
Getting Help
|
||||
============
|
||||
|
||||
Documentation for django-model-utils is available at https://django-model-utils.readthedocs.io/
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Please file bugs and send pull requests to the `GitHub repository`_ and `issue
|
||||
tracker`_. See `CONTRIBUTING.rst`_ for details.
|
||||
|
||||
.. _GitHub repository: https://github.com/carljm/django-model-utils/
|
||||
.. _issue tracker: https://github.com/carljm/django-model-utils/issues
|
||||
.. _CONTRIBUTING.rst: https://github.com/carljm/django-model-utils/blob/master/CONTRIBUTING.rst
|
|
@ -0,0 +1,11 @@
|
|||
# see git-dpm(1) from git-dpm package
|
||||
777393fbb8b0684d2a5fe1095c4ff98965eb1238
|
||||
777393fbb8b0684d2a5fe1095c4ff98965eb1238
|
||||
3896a3775fa50d001bf7fe4104550e3055fb2cc0
|
||||
3896a3775fa50d001bf7fe4104550e3055fb2cc0
|
||||
django-model-utils_2.5.2.orig.tar.gz
|
||||
d441ebadb9f6594fe5c385fa516bad022d604a61
|
||||
41124
|
||||
debianTag="debian/%e%v"
|
||||
patchedTag="patched/%e%v"
|
||||
upstreamTag="upstream/%e%u"
|
|
@ -0,0 +1,96 @@
|
|||
django-model-utils (2.5.2-2) unstable; urgency=medium
|
||||
|
||||
[ Ben Finney ]
|
||||
* Improve package description. Closes: #834116, Closes: #834117.
|
||||
|
||||
[ Brian May ]
|
||||
* New upstream release.
|
||||
* Fix FTBFS: Closes: #828651.
|
||||
* Update standards version to 3.9.8.
|
||||
|
||||
-- Brian May <bam@debian.org> Sun, 11 Sep 2016 12:56:36 +1000
|
||||
|
||||
django-model-utils (2.4-1) unstable; urgency=medium
|
||||
|
||||
* New upstream issues.
|
||||
* Fixes FTBFS with Django 1.9. Closes: #806350.
|
||||
|
||||
-- Brian May <bam@debian.org> Thu, 03 Dec 2015 09:03:21 +1100
|
||||
|
||||
django-model-utils (2.3.1-3) unstable; urgency=medium
|
||||
|
||||
* Run tests for Python 3.4 and Python 3.5.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 26 Oct 2015 13:26:34 +1100
|
||||
|
||||
django-model-utils (2.3.1-2) unstable; urgency=medium
|
||||
|
||||
* Add VCS-* headers.
|
||||
* Update Standards-Version to 3.9.6.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 12 Oct 2015 18:03:27 +1100
|
||||
|
||||
django-model-utils (2.3.1-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version.
|
||||
* Swap maintainer and uploaders. Maintainer now set to DPMT.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 12 Oct 2015 16:28:31 +1100
|
||||
|
||||
django-model-utils (2.0.3-2) unstable; urgency=low
|
||||
|
||||
* Python3 package.
|
||||
|
||||
-- Brian May <bam@debian.org> Thu, 03 Jul 2014 16:07:56 +1000
|
||||
|
||||
django-model-utils (2.0.3-1) unstable; urgency=low
|
||||
|
||||
* New upstream release. Closes: #739578.
|
||||
* Take over package from Jonas Smedegaard. Thanks to his contribution to
|
||||
Debian.
|
||||
* Remove reference to git repository, it doesn't appear to exist.
|
||||
* Update to dh_python2.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 02 May 2014 09:58:50 +1000
|
||||
|
||||
django-model-utils (1.3.1-1) unstable; urgency=low
|
||||
|
||||
[ upstream ]
|
||||
* New release.
|
||||
+ Moved primary development from Bitbucket to GitHub.
|
||||
+ Removed deprecated ChoiceEnum, InheritanceCastModel,
|
||||
InheritanceCastManager, and manager_from.
|
||||
+ Fixed pickling of PassThroughManager.
|
||||
+ Set use_for_related_fields = True on QueryManager.
|
||||
+ Added __len__ method to Choices.
|
||||
+ Fixed InheritanceQuerySet on Django 1.5.
|
||||
+ Allow specifying default value for a StatusField.
|
||||
+ Fix calling create() on a RelatedManager that subclasses a dynamic
|
||||
PassThroughManager.
|
||||
+ Add workaround in InheritanceQuerySet to avoid overriding prior
|
||||
calls to select_related().
|
||||
+ Added support for arbitrary levels of model inheritance in
|
||||
InheritanceManager.
|
||||
+ Added ModelTracker for tracking field changes between model saves.
|
||||
+ Added explicit default to BooleanField in tests, for Django trunk
|
||||
compatibility.
|
||||
+ Fix intermittent StatusField bug.
|
||||
Closes: bug#711358. Thanks to Jakub Wilk.
|
||||
|
||||
[ Jonas Smedegaard ]
|
||||
* Update README.source to emphasize control.in file as *not* a
|
||||
show-stopper for contributions, referring to wiki page for details.
|
||||
* Update upstream Homepage and source to github.
|
||||
* Bump standards-version to 3.9.4.
|
||||
* Extend coverage for main copyright holder.
|
||||
* Bump packaging license to GPL-3+, and extend coverage to include
|
||||
current year.
|
||||
|
||||
-- Jonas Smedegaard <dr@jones.dk> Thu, 06 Jun 2013 21:12:17 +0200
|
||||
|
||||
django-model-utils (1.1.0-1) unstable; urgency=low
|
||||
|
||||
* Initial packaging release.
|
||||
Closes: bug#694583.
|
||||
|
||||
-- Jonas Smedegaard <dr@jones.dk> Wed, 28 Nov 2012 02:27:06 +0100
|
|
@ -0,0 +1 @@
|
|||
*.egg-info/*
|
|
@ -0,0 +1 @@
|
|||
8
|
|
@ -0,0 +1,46 @@
|
|||
Source: django-model-utils
|
||||
Section: python
|
||||
Priority: extra
|
||||
Maintainer: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
|
||||
Uploaders: Brian May <bam@debian.org>
|
||||
Build-Depends: debhelper (>=8.1.0), dh-python,
|
||||
python-all, python-setuptools, python-django,
|
||||
python3-all (>= 2.6.6-3~), python3-setuptools, python3-django
|
||||
Standards-Version: 3.9.8
|
||||
Homepage: https://github.com/carljm/django-model-utils/
|
||||
Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/django-model-utils.git
|
||||
Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/django-model-utils.git
|
||||
|
||||
Package: python-django-model-utils
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}
|
||||
Description: Django model mixins and utilities — Python 2
|
||||
Django is a high-level web application framework that loosely follows
|
||||
the model-view-controller design pattern.
|
||||
.
|
||||
The ‘django-model-utils’ library provides some mixins and utilities
|
||||
for Django:
|
||||
.
|
||||
* QueryManager: one-line definition of Managers returning a
|
||||
custom QuerySet.
|
||||
* InheritanceCastModel: more efficient use of model inheritance
|
||||
* TimeStampedModel: self-updating ‘created’ and ‘modified’ fields.
|
||||
.
|
||||
This package installs the library for Python 2.
|
||||
|
||||
Package: python3-django-model-utils
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python3:Depends}
|
||||
Description: Django model mixins and utilities — Python 3
|
||||
Django is a high-level web application framework that loosely follows
|
||||
the model-view-controller design pattern.
|
||||
.
|
||||
The ‘django-model-utils’ library provides some mixins and utilities
|
||||
for Django:
|
||||
.
|
||||
* QueryManager: one-line definition of Managers returning a
|
||||
custom QuerySet.
|
||||
* InheritanceCastModel: more efficient use of model inheritance
|
||||
* TimeStampedModel: self-updating ‘created’ and ‘modified’ fields.
|
||||
.
|
||||
This package installs the library for Python 3.
|
|
@ -0,0 +1,53 @@
|
|||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: django-model-utils
|
||||
Upstream-Contact: https://github.com/carljm/django-model-utils/issues/
|
||||
Source: https://github.com/carljm/django-model-utils/downloads/
|
||||
git://github.com/carljm/django-model-utils/
|
||||
|
||||
Files: *
|
||||
Copyright: 2009-2013, Carl Meyer <carl@dirtcircle.com>
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2014, Brian May <bam@debian.org>
|
||||
License: GPL-3+
|
||||
|
||||
License: BSD-3-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
.
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* 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.
|
||||
* Neither the name of the author nor the names of other 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
|
||||
OWNER 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.
|
||||
|
||||
License: GPL-3+
|
||||
This file is free software; you can redistribute it and/or modify it
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation; either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
.
|
||||
Comment:
|
||||
.
|
||||
On Debian systems the 'GNU General Public License' version 3 is located
|
||||
in '/usr/share/common-licenses/GPL-3'.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,3 @@
|
|||
AUTHORS.rst
|
||||
CONTRIBUTING.rst
|
||||
README.rst
|
|
@ -0,0 +1,85 @@
|
|||
From 777393fbb8b0684d2a5fe1095c4ff98965eb1238 Mon Sep 17 00:00:00 2001
|
||||
From: Brian May <bam@debian.org>
|
||||
Date: Sun, 11 Sep 2016 12:21:37 +1000
|
||||
Subject: Fix FieldTracker failing with Django 1.10
|
||||
|
||||
From upstream pull request
|
||||
https://github.com/carljm/django-model-utils/issues/232
|
||||
---
|
||||
model_utils/tracker.py | 41 ++++++++++++++++++++++++++++-------------
|
||||
1 file changed, 28 insertions(+), 13 deletions(-)
|
||||
|
||||
diff --git a/model_utils/tracker.py b/model_utils/tracker.py
|
||||
index a9c7f70..6aa7d8a 100644
|
||||
--- a/model_utils/tracker.py
|
||||
+++ b/model_utils/tracker.py
|
||||
@@ -2,8 +2,9 @@ from __future__ import unicode_literals
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
-from django.db import models
|
||||
+import django
|
||||
from django.core.exceptions import FieldError
|
||||
+from django.db import models
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
|
||||
|
||||
@@ -62,12 +63,14 @@ class FieldInstanceTracker(object):
|
||||
)
|
||||
|
||||
def init_deferred_fields(self):
|
||||
- self.instance._deferred_fields = []
|
||||
+ self.instance._deferred_fields = set()
|
||||
if hasattr(self.instance, '_deferred') and not self.instance._deferred:
|
||||
return
|
||||
|
||||
class DeferredAttributeTracker(DeferredAttribute):
|
||||
def __get__(field, instance, owner):
|
||||
+ if instance is None:
|
||||
+ return field
|
||||
data = instance.__dict__
|
||||
if data.get(field.field_name, field) is field:
|
||||
instance._deferred_fields.remove(field.field_name)
|
||||
@@ -76,19 +79,31 @@ class FieldInstanceTracker(object):
|
||||
self.saved_data[field.field_name] = deepcopy(value)
|
||||
return data[field.field_name]
|
||||
|
||||
- for field in self.fields:
|
||||
- field_obj = self.instance.__class__.__dict__.get(field)
|
||||
- if isinstance(field_obj, DeferredAttribute):
|
||||
- self.instance._deferred_fields.append(field)
|
||||
-
|
||||
- # Django 1.4
|
||||
- model = None
|
||||
- if hasattr(field_obj, 'model_ref'):
|
||||
- model = field_obj.model_ref()
|
||||
-
|
||||
+ if django.VERSION >= (1, 8):
|
||||
+ self.instance._deferred_fields = self.instance.get_deferred_fields()
|
||||
+ for field in self.instance._deferred_fields:
|
||||
+ if django.VERSION >= (1, 10):
|
||||
+ field_obj = getattr(self.instance.__class__, field)
|
||||
+ else:
|
||||
+ field_obj = self.instance.__class__.__dict__.get(field)
|
||||
field_tracker = DeferredAttributeTracker(
|
||||
- field_obj.field_name, model)
|
||||
+ field_obj.field_name, None)
|
||||
setattr(self.instance.__class__, field, field_tracker)
|
||||
+ else:
|
||||
+ for field in self.fields:
|
||||
+ field_obj = self.instance.__class__.__dict__.get(field)
|
||||
+ if isinstance(field_obj, DeferredAttribute):
|
||||
+ self.instance._deferred_fields.add(field)
|
||||
+
|
||||
+ # Django 1.4
|
||||
+ if django.VERSION >= (1, 5):
|
||||
+ model = None
|
||||
+ else:
|
||||
+ model = field_obj.model_ref()
|
||||
+
|
||||
+ field_tracker = DeferredAttributeTracker(
|
||||
+ field_obj.field_name, model)
|
||||
+ setattr(self.instance.__class__, field, field_tracker)
|
||||
|
||||
|
||||
class FieldTracker(object):
|
|
@ -0,0 +1 @@
|
|||
0001-Fix-FieldTracker-failing-with-Django-1.10.patch
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
export PYBUILD_NAME=django-model-utils
|
||||
|
||||
%:
|
||||
dh $@ --with python2,python3 --buildsystem=pybuild
|
||||
|
||||
.PHONY: override_dh_auto_test
|
||||
override_dh_auto_test:
|
||||
dh_auto_test -- --system=custom --test-args="{interpreter} ./runtests.py"
|
||||
|
||||
.PHONY: override_dh_installchangelogs
|
||||
override_dh_installchangelogs:
|
||||
dh_installchangelogs CHANGES.rst
|
|
@ -0,0 +1 @@
|
|||
3.0 (quilt)
|
|
@ -0,0 +1,3 @@
|
|||
# Run the "uscan" command to check for upstream updates and more.
|
||||
version=3
|
||||
https://github.com/carljm/django-model-utils/tags .*/archive/v?(\d[\d\.]+).tar.gz
|
|
@ -0,0 +1,177 @@
|
|||
# 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 gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> 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 " 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)"
|
||||
|
||||
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-model-utils.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-model-utils.qhc"
|
||||
|
||||
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-model-utils"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-model-utils"
|
||||
@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."
|
||||
|
||||
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."
|
|
@ -0,0 +1,256 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-model-utils documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Jul 31 22:27:07 2013.
|
||||
#
|
||||
# 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, os
|
||||
|
||||
# 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.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
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-model-utils'
|
||||
copyright = u'2015, Carl Meyer'
|
||||
|
||||
parent_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
def get_version():
|
||||
with open(os.path.join(parent_dir, 'model_utils', '__init__.py')) as f:
|
||||
for line in f:
|
||||
if line.startswith('__version__ ='):
|
||||
return line.split('=')[1].strip().strip('"\'')
|
||||
|
||||
# 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 full version, including alpha/beta/rc tags.
|
||||
release = get_version()
|
||||
# The short X.Y version.
|
||||
version = release
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#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
|
||||
|
||||
|
||||
# -- 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 = 'default'
|
||||
|
||||
# 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
|
||||
# "<project> v<release> 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']
|
||||
|
||||
# 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 <link> 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
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-model-utilsdoc'
|
||||
|
||||
|
||||
# -- 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': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-model-utils.tex', u'django-model-utils Documentation',
|
||||
u'Carl Meyer', '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 = [
|
||||
('index', 'django-model-utils', u'django-model-utils Documentation',
|
||||
[u'Carl Meyer'], 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 = [
|
||||
('index', 'django-model-utils', u'django-model-utils Documentation',
|
||||
u'Carl Meyer', 'django-model-utils', '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
|
|
@ -0,0 +1,156 @@
|
|||
Fields
|
||||
======
|
||||
|
||||
.. _StatusField:
|
||||
|
||||
StatusField
|
||||
-----------
|
||||
|
||||
A simple convenience for giving a model a set of "states."
|
||||
``StatusField`` is a ``CharField`` subclass that expects to find a
|
||||
class attribute called ``STATUS`` on its model or you can pass
|
||||
``choices_name`` to use a different attribute name, and uses that as
|
||||
its ``choices``. Also sets a default ``max_length`` of 100, and sets
|
||||
its default value to the first item in the ``STATUS`` choices:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.fields import StatusField
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices('draft', 'published')
|
||||
# ...
|
||||
status = StatusField()
|
||||
|
||||
(The ``STATUS`` class attribute does not have to be a :ref:`Choices`
|
||||
instance, it can be an ordinary list of two-tuples).
|
||||
|
||||
Using a different name for the model's choices class attribute
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.fields import StatusField
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
ANOTHER_CHOICES = Choices('draft', 'published')
|
||||
# ...
|
||||
another_field = StatusField(choices_name='ANOTHER_CHOICES')
|
||||
|
||||
``StatusField`` does not set ``db_index=True`` automatically; if you
|
||||
expect to frequently filter on your status field (and it will have
|
||||
enough selectivity to make an index worthwhile) you may want to add this
|
||||
yourself.
|
||||
|
||||
|
||||
.. _MonitorField:
|
||||
|
||||
MonitorField
|
||||
------------
|
||||
|
||||
A ``DateTimeField`` subclass that monitors another field on the model,
|
||||
and updates itself to the current date-time whenever the monitored
|
||||
field changes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.fields import MonitorField, StatusField
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices('draft', 'published')
|
||||
|
||||
status = StatusField()
|
||||
status_changed = MonitorField(monitor='status')
|
||||
|
||||
(A ``MonitorField`` can monitor any type of field for changes, not only a
|
||||
``StatusField``.)
|
||||
|
||||
If a list is passed to the ``when`` parameter, the field will only
|
||||
update when it matches one of the specified values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.fields import MonitorField, StatusField
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices('draft', 'published')
|
||||
|
||||
status = StatusField()
|
||||
published_at = MonitorField(monitor='status', when=['published'])
|
||||
|
||||
|
||||
SplitField
|
||||
----------
|
||||
|
||||
A ``TextField`` subclass that automatically pulls an excerpt out of
|
||||
its content (based on a "split here" marker or a default number of
|
||||
initial paragraphs) and stores both its content and excerpt values in
|
||||
the database.
|
||||
|
||||
A ``SplitField`` is easy to add to any model definition:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.db import models
|
||||
from model_utils.fields import SplitField
|
||||
|
||||
class Article(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
body = SplitField()
|
||||
|
||||
``SplitField`` automatically creates an extra non-editable field
|
||||
``_body_excerpt`` to store the excerpt. This field doesn't need to be
|
||||
accessed directly; see below.
|
||||
|
||||
|
||||
Accessing a SplitField on a model
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When accessing an attribute of a model that was declared as a
|
||||
``SplitField``, a ``SplitText`` object is returned. The ``SplitText``
|
||||
object has three attributes:
|
||||
|
||||
``content``:
|
||||
The full field contents.
|
||||
``excerpt``:
|
||||
The excerpt of ``content`` (read-only).
|
||||
``has_more``:
|
||||
True if the excerpt and content are different, False otherwise.
|
||||
|
||||
This object also has a ``__unicode__`` method that returns the full
|
||||
content, allowing ``SplitField`` attributes to appear in templates
|
||||
without having to access ``content`` directly.
|
||||
|
||||
Assuming the ``Article`` model above:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a = Article.objects.all()[0]
|
||||
>>> a.body.content
|
||||
u'some text\n\n<!-- split -->\n\nmore text'
|
||||
>>> a.body.excerpt
|
||||
u'some text\n'
|
||||
>>> unicode(a.body)
|
||||
u'some text\n\n<!-- split -->\n\nmore text'
|
||||
|
||||
Assignment to ``a.body`` is equivalent to assignment to
|
||||
``a.body.content``.
|
||||
|
||||
.. note::
|
||||
|
||||
a.body.excerpt is only updated when a.save() is called
|
||||
|
||||
|
||||
Customized excerpting
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default, ``SplitField`` looks for the marker ``<!-- split -->``
|
||||
alone on a line and takes everything before that marker as the
|
||||
excerpt. This marker can be customized by setting the ``SPLIT_MARKER``
|
||||
setting.
|
||||
|
||||
If no marker is found in the content, the first two paragraphs (where
|
||||
paragraphs are blocks of text separated by a blank line) are taken to
|
||||
be the excerpt. This number can be customized by setting the
|
||||
``SPLIT_DEFAULT_PARAGRAPHS`` setting.
|
|
@ -0,0 +1,38 @@
|
|||
==================
|
||||
django-model-utils
|
||||
==================
|
||||
|
||||
Django model mixins and utilities.
|
||||
|
||||
|
||||
Contents
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
setup
|
||||
fields
|
||||
models
|
||||
managers
|
||||
utilities
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Please file bugs and send pull requests to the `GitHub repository`_ and `issue
|
||||
tracker`_.
|
||||
|
||||
.. _GitHub repository: https://github.com/carljm/django-model-utils/
|
||||
.. _issue tracker: https://github.com/carljm/django-model-utils/issues
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> 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. 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. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over 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
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-model-utils.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-model-utils.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
|
@ -0,0 +1,180 @@
|
|||
Model Managers
|
||||
==============
|
||||
|
||||
InheritanceManager
|
||||
------------------
|
||||
|
||||
This manager (`contributed by Jeff Elmore`_) should be attached to a base model
|
||||
class in a model-inheritance tree. It allows queries on that base model to
|
||||
return heterogenous results of the actual proper subtypes, without any
|
||||
additional queries.
|
||||
|
||||
For instance, if you have a ``Place`` model with subclasses ``Restaurant`` and
|
||||
``Bar``, you may want to query all Places:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
nearby_places = Place.objects.filter(location='here')
|
||||
|
||||
But when you iterate over ``nearby_places``, you'll get only ``Place``
|
||||
instances back, even for objects that are "really" ``Restaurant`` or ``Bar``.
|
||||
If you attach an ``InheritanceManager`` to ``Place``, you can just call the
|
||||
``select_subclasses()`` method on the ``InheritanceManager`` or any
|
||||
``QuerySet`` from it, and the resulting objects will be instances of
|
||||
``Restaurant`` or ``Bar``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
class Place(models.Model):
|
||||
# ...
|
||||
objects = InheritanceManager()
|
||||
|
||||
class Restaurant(Place):
|
||||
# ...
|
||||
|
||||
class Bar(Place):
|
||||
# ...
|
||||
|
||||
nearby_places = Place.objects.filter(location='here').select_subclasses()
|
||||
for place in nearby_places:
|
||||
# "place" will automatically be an instance of Place, Restaurant, or Bar
|
||||
|
||||
The database query performed will have an extra join for each subclass; if you
|
||||
want to reduce the number of joins and you only need particular subclasses to
|
||||
be returned as their actual type, you can pass subclass names to
|
||||
``select_subclasses()``, much like the built-in ``select_related()`` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
nearby_places = Place.objects.select_subclasses("restaurant")
|
||||
# restaurants will be Restaurant instances, bars will still be Place instances
|
||||
|
||||
nearby_places = Place.objects.select_subclasses("restaurant", "bar")
|
||||
# all Places will be converted to Restaurant and Bar instances.
|
||||
|
||||
It is also possible to use the subclasses themselves as arguments to
|
||||
``select_subclasses``, leaving it to calculate the relationship for you:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
nearby_places = Place.objects.select_subclasses(Restaurant)
|
||||
# restaurants will be Restaurant instances, bars will still be Place instances
|
||||
|
||||
nearby_places = Place.objects.select_subclasses(Restaurant, Bar)
|
||||
# all Places will be converted to Restaurant and Bar instances.
|
||||
|
||||
It is even possible to mix and match the two:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
nearby_places = Place.objects.select_subclasses(Restaurant, "bar")
|
||||
# all Places will be converted to Restaurant and Bar instances.
|
||||
|
||||
``InheritanceManager`` also provides a subclass-fetching alternative to the
|
||||
``get()`` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
place = Place.objects.get_subclass(id=some_id)
|
||||
# "place" will automatically be an instance of Place, Restaurant, or Bar
|
||||
|
||||
If you don't explicitly call ``select_subclasses()`` or ``get_subclass()``,
|
||||
an ``InheritanceManager`` behaves identically to a normal ``Manager``; so
|
||||
it's safe to use as your default manager for the model.
|
||||
|
||||
.. note::
|
||||
|
||||
Due to `Django bug #16572`_, on Django versions prior to 1.6
|
||||
``InheritanceManager`` only supports a single level of model inheritance;
|
||||
it won't work for grandchild models.
|
||||
|
||||
.. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/
|
||||
.. _Django bug #16572: https://code.djangoproject.com/ticket/16572
|
||||
|
||||
|
||||
.. _QueryManager:
|
||||
|
||||
QueryManager
|
||||
------------
|
||||
|
||||
Many custom model managers do nothing more than return a QuerySet that
|
||||
is filtered in some way. ``QueryManager`` allows you to express this
|
||||
pattern with a minimum of boilerplate:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.db import models
|
||||
from model_utils.managers import QueryManager
|
||||
|
||||
class Post(models.Model):
|
||||
...
|
||||
published = models.BooleanField()
|
||||
pub_date = models.DateField()
|
||||
...
|
||||
|
||||
objects = models.Manager()
|
||||
public = QueryManager(published=True).order_by('-pub_date')
|
||||
|
||||
The kwargs passed to ``QueryManager`` will be passed as-is to the
|
||||
``QuerySet.filter()`` method. You can also pass a ``Q`` object to
|
||||
``QueryManager`` to express more complex conditions. Note that you can
|
||||
set the ordering of the ``QuerySet`` returned by the ``QueryManager``
|
||||
by chaining a call to ``.order_by()`` on the ``QueryManager`` (this is
|
||||
not required).
|
||||
|
||||
|
||||
PassThroughManager
|
||||
------------------
|
||||
|
||||
`PassThroughManager` was removed in django-model-utils 2.4. Use Django's
|
||||
built-in `QuerySet.as_manager()` and/or `Manager.from_queryset()` utilities
|
||||
instead.
|
||||
|
||||
Mixins
|
||||
------
|
||||
|
||||
Each of the above manager classes has a corresponding mixin that can be used to
|
||||
add functionality to any manager. For example, to create a GeoDjango
|
||||
``GeoManager`` that includes "pass through" functionality, you can write the
|
||||
following code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.contrib.gis.db import models
|
||||
from django.contrib.gis.db.models.query import GeoQuerySet
|
||||
|
||||
from model_utils.managers import PassThroughManagerMixin
|
||||
|
||||
class PassThroughGeoManager(PassThroughManagerMixin, models.GeoManager):
|
||||
pass
|
||||
|
||||
class LocationQuerySet(GeoQuerySet):
|
||||
def within_boundary(self, geom):
|
||||
return self.filter(point__within=geom)
|
||||
|
||||
def public(self):
|
||||
return self.filter(public=True)
|
||||
|
||||
class Location(models.Model):
|
||||
point = models.PointField()
|
||||
public = models.BooleanField(default=True)
|
||||
objects = PassThroughGeoManager.for_queryset_class(LocationQuerySet)()
|
||||
|
||||
Location.objects.public()
|
||||
Location.objects.within_boundary(geom=geom)
|
||||
Location.objects.within_boundary(geom=geom).public()
|
||||
|
||||
|
||||
Now you have a "pass through manager" that can also take advantage of
|
||||
GeoDjango's spatial lookups. You can similarly add additional functionality to
|
||||
any manager by composing that manager with ``InheritanceManagerMixin`` or
|
||||
``QueryManagerMixin``.
|
||||
|
||||
(Note that any manager class using ``InheritanceManagerMixin`` must return a
|
||||
``QuerySet`` class using ``InheritanceQuerySetMixin`` from its ``get_queryset``
|
||||
method. This means that if composing ``InheritanceManagerMixin`` and
|
||||
``PassThroughManagerMixin``, the ``QuerySet`` class passed to
|
||||
``PassThroughManager.for_queryset_class`` must inherit
|
||||
``InheritanceQuerySetMixin``.)
|
|
@ -0,0 +1,49 @@
|
|||
Models
|
||||
======
|
||||
|
||||
TimeFramedModel
|
||||
---------------
|
||||
|
||||
An abstract base class for any model that expresses a time-range. Adds
|
||||
``start`` and ``end`` nullable DateTimeFields, and a ``timeframed``
|
||||
manager that returns only objects for whom the current date-time lies
|
||||
within their time range.
|
||||
|
||||
|
||||
TimeStampedModel
|
||||
----------------
|
||||
|
||||
This abstract base class just provides self-updating ``created`` and
|
||||
``modified`` fields on any model that inherits from it.
|
||||
|
||||
|
||||
StatusModel
|
||||
-----------
|
||||
|
||||
Pulls together :ref:`StatusField`, :ref:`MonitorField` and :ref:`QueryManager`
|
||||
into an abstract base class for any model with a "status."
|
||||
|
||||
Just provide a ``STATUS`` class-attribute (a :ref:`Choices` object or a
|
||||
list of two-tuples), and your model will have a ``status`` field with
|
||||
those choices, a ``status_changed`` field containing the date-time the
|
||||
``status`` was last changed, and a manager for each status that
|
||||
returns objects with that status only:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils.models import StatusModel
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(StatusModel):
|
||||
STATUS = Choices('draft', 'published')
|
||||
|
||||
# ...
|
||||
|
||||
a = Article()
|
||||
a.status = Article.STATUS.published
|
||||
|
||||
# this save will update a.status_changed
|
||||
a.save()
|
||||
|
||||
# this query will only return published articles:
|
||||
Article.published.all()
|
|
@ -0,0 +1,23 @@
|
|||
=====
|
||||
Setup
|
||||
=====
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Install from PyPI with ``pip``::
|
||||
|
||||
pip install django-model-utils
|
||||
|
||||
To use ``django-model-utils`` in your Django project, just import and
|
||||
use the utility classes described in this documentation; there is no need to
|
||||
modify your ``INSTALLED_APPS`` setting.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
``django-model-utils`` supports `Django`_ 1.4.2 and later on Python 2.6, 2.7,
|
||||
3.2, and 3.3.
|
||||
|
||||
.. _Django: http://www.djangoproject.com/
|
|
@ -0,0 +1,225 @@
|
|||
=======================
|
||||
Miscellaneous Utilities
|
||||
=======================
|
||||
|
||||
.. _Choices:
|
||||
|
||||
Choices
|
||||
=======
|
||||
|
||||
``Choices`` provides some conveniences for setting ``choices`` on a Django model field:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices('draft', 'published')
|
||||
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
|
||||
|
||||
A ``Choices`` object is initialized with any number of choices. In the
|
||||
simplest case, each choice is a string; that string will be used both
|
||||
as the database representation of the choice, and the human-readable
|
||||
representation. Note that you can access options as attributes on the
|
||||
``Choices`` object: ``STATUS.draft``.
|
||||
|
||||
But you may want your human-readable versions translated, in which
|
||||
case you need to separate the human-readable version from the DB
|
||||
representation. In this case you can provide choices as two-tuples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices(('draft', _('draft')), ('published', _('published')))
|
||||
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
|
||||
|
||||
But what if your database representation of choices is constrained in
|
||||
a way that would hinder readability of your code? For instance, you
|
||||
may need to use an ``IntegerField`` rather than a ``CharField``, or
|
||||
you may want the database to order the values in your field in some
|
||||
specific way. In this case, you can provide your choices as triples,
|
||||
where the first element is the database representation, the second is
|
||||
a valid Python identifier you will use in your code as a constant, and
|
||||
the third is the human-readable version:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices((0, 'draft', _('draft')), (1, 'published', _('published')))
|
||||
status = models.IntegerField(choices=STATUS, default=STATUS.draft)
|
||||
|
||||
You can index into a ``Choices`` instance to translate a database
|
||||
representation to its display name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
status_display = Article.STATUS[article.status]
|
||||
|
||||
Option groups can also be used with ``Choices``; in that case each
|
||||
argument is a tuple consisting of the option group name and a list of
|
||||
options, where each option in the list is either a string, a two-tuple,
|
||||
or a triple as outlined above. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = Choices(('Visible', ['new', 'archived']), ('Invisible', ['draft', 'deleted']))
|
||||
|
||||
Choices can be concatenated with the ``+`` operator, both to other Choices
|
||||
instances and other iterable objects that could be converted into Choices:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from model_utils import Choices
|
||||
|
||||
GENERIC_CHOICES = Choices((0, 'draft', _('draft')), (1, 'published', _('published')))
|
||||
|
||||
class Article(models.Model):
|
||||
STATUS = GENERIC_CHOICES + [(2, 'featured', _('featured'))]
|
||||
status = models.IntegerField(choices=STATUS, default=STATUS.draft)
|
||||
|
||||
|
||||
Field Tracker
|
||||
=============
|
||||
|
||||
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 FieldTracker
|
||||
|
||||
class Post(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
body = models.TextField()
|
||||
|
||||
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)
|
||||
|
||||
* 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:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a = Post.objects.create(title='First Post')
|
||||
>>> a.title = 'Welcome'
|
||||
>>> a.tracker.previous('title')
|
||||
u'First Post'
|
||||
|
||||
Returns ``None`` when the model instance isn't saved yet.
|
||||
|
||||
|
||||
has_changed
|
||||
~~~~~~~~~~~
|
||||
Returns ``True`` if the given field has changed since the last save. The ``has_changed`` method expects a single field:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a = Post.objects.create(title='First Post')
|
||||
>>> a.title = 'Welcome'
|
||||
>>> a.tracker.has_changed('title')
|
||||
True
|
||||
>>> a.tracker.has_changed('body')
|
||||
False
|
||||
|
||||
The ``has_changed`` method relies on ``previous`` to determine whether a
|
||||
field's values has changed.
|
||||
|
||||
|
||||
changed
|
||||
~~~~~~~
|
||||
Returns a dictionary of all fields that have been changed since the last save
|
||||
and the values of the fields during the last save:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a = Post.objects.create(title='First Post')
|
||||
>>> a.title = 'Welcome'
|
||||
>>> a.body = 'First post!'
|
||||
>>> a.tracker.changed()
|
||||
{'title': 'First Post', 'body': ''}
|
||||
|
||||
The ``changed`` method relies on ``has_changed`` to determine which fields
|
||||
have changed.
|
||||
|
||||
|
||||
Tracking 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 FieldTracker
|
||||
|
||||
class Post(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
body = models.TextField()
|
||||
|
||||
title_tracker = FieldTracker(fields=['title'])
|
||||
|
||||
An example using the model specified above:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> a = Post.objects.create(title='First Post')
|
||||
>>> a.body = 'First post!'
|
||||
>>> a.title_tracker.changed()
|
||||
{'title': None}
|
||||
|
||||
|
||||
Checking changes using signals
|
||||
------------------------------
|
||||
|
||||
The field tracker methods may also be used in ``pre_save`` and ``post_save``
|
||||
signal handlers to identify field changes on model save.
|
||||
|
||||
.. NOTE::
|
||||
|
||||
Due to the implementation of ``FieldTracker``, ``post_save`` signal
|
||||
handlers relying on field tracker methods should only be registered after
|
||||
model creation.
|
|
@ -0,0 +1,4 @@
|
|||
from .choices import Choices
|
||||
from .tracker import FieldTracker, ModelTracker
|
||||
|
||||
__version__ = '2.5.2'
|
|
@ -0,0 +1,153 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
class Choices(object):
|
||||
"""
|
||||
A class to encapsulate handy functionality for lists of choices
|
||||
for a Django model field.
|
||||
|
||||
Each argument to ``Choices`` is a choice, represented as either a
|
||||
string, a two-tuple, or a three-tuple.
|
||||
|
||||
If a single string is provided, that string is used as the
|
||||
database representation of the choice as well as the
|
||||
human-readable presentation.
|
||||
|
||||
If a two-tuple is provided, the first item is used as the database
|
||||
representation and the second the human-readable presentation.
|
||||
|
||||
If a triple is provided, the first item is the database
|
||||
representation, the second a valid Python identifier that can be
|
||||
used as a readable label in code, and the third the human-readable
|
||||
presentation. This is most useful when the database representation
|
||||
must sacrifice readability for some reason: to achieve a specific
|
||||
ordering, to use an integer rather than a character field, etc.
|
||||
|
||||
Regardless of what representation of each choice is originally
|
||||
given, when iterated over or indexed into, a ``Choices`` object
|
||||
behaves as the standard Django choices list of two-tuples.
|
||||
|
||||
If the triple form is used, the Python identifier names can be
|
||||
accessed as attributes on the ``Choices`` object, returning the
|
||||
database representation. (If the single or two-tuple forms are
|
||||
used and the database representation happens to be a valid Python
|
||||
identifier, the database representation itself is available as an
|
||||
attribute on the ``Choices`` object, returning itself.)
|
||||
|
||||
Option groups can also be used with ``Choices``; in that case each
|
||||
argument is a tuple consisting of the option group name and a list
|
||||
of options, where each option in the list is either a string, a
|
||||
two-tuple, or a triple as outlined above.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *choices):
|
||||
# list of choices expanded to triples - can include optgroups
|
||||
self._triples = []
|
||||
# list of choices as (db, human-readable) - can include optgroups
|
||||
self._doubles = []
|
||||
# dictionary mapping db representation to human-readable
|
||||
self._display_map = {}
|
||||
# dictionary mapping Python identifier to db representation
|
||||
self._identifier_map = {}
|
||||
# set of db representations
|
||||
self._db_values = set()
|
||||
|
||||
self._process(choices)
|
||||
|
||||
|
||||
def _store(self, triple, triple_collector, double_collector):
|
||||
self._identifier_map[triple[1]] = triple[0]
|
||||
self._display_map[triple[0]] = triple[2]
|
||||
self._db_values.add(triple[0])
|
||||
triple_collector.append(triple)
|
||||
double_collector.append((triple[0], triple[2]))
|
||||
|
||||
|
||||
def _process(self, choices, triple_collector=None, double_collector=None):
|
||||
if triple_collector is None:
|
||||
triple_collector = self._triples
|
||||
if double_collector is None:
|
||||
double_collector = self._doubles
|
||||
|
||||
store = lambda c: self._store(c, triple_collector, double_collector)
|
||||
|
||||
for choice in choices:
|
||||
if isinstance(choice, (list, tuple)):
|
||||
if len(choice) == 3:
|
||||
store(choice)
|
||||
elif len(choice) == 2:
|
||||
if isinstance(choice[1], (list, tuple)):
|
||||
# option group
|
||||
group_name = choice[0]
|
||||
subchoices = choice[1]
|
||||
tc = []
|
||||
triple_collector.append((group_name, tc))
|
||||
dc = []
|
||||
double_collector.append((group_name, dc))
|
||||
self._process(subchoices, tc, dc)
|
||||
else:
|
||||
store((choice[0], choice[0], choice[1]))
|
||||
else:
|
||||
raise ValueError(
|
||||
"Choices can't take a list of length %s, only 2 or 3"
|
||||
% len(choice)
|
||||
)
|
||||
else:
|
||||
store((choice, choice, choice))
|
||||
|
||||
|
||||
def __len__(self):
|
||||
return len(self._doubles)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._doubles)
|
||||
|
||||
|
||||
def __getattr__(self, attname):
|
||||
try:
|
||||
return self._identifier_map[attname]
|
||||
except KeyError:
|
||||
raise AttributeError(attname)
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._display_map[key]
|
||||
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
other = other._triples
|
||||
else:
|
||||
other = list(other)
|
||||
return Choices(*(self._triples + other))
|
||||
|
||||
|
||||
def __radd__(self, other):
|
||||
# radd is never called for matching types, so we don't check here
|
||||
other = list(other)
|
||||
return Choices(*(other + self._triples))
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self._triples == other._triples
|
||||
return False
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (
|
||||
self.__class__.__name__,
|
||||
', '.join(("%s" % repr(i) for i in self._triples))
|
||||
)
|
||||
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._db_values
|
||||
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return self.__class__(*copy.deepcopy(self._triples, memo))
|
|
@ -0,0 +1,267 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.timezone import now
|
||||
|
||||
DEFAULT_CHOICES_NAME = 'STATUS'
|
||||
|
||||
|
||||
class AutoCreatedField(models.DateTimeField):
|
||||
"""
|
||||
A DateTimeField that automatically populates itself at
|
||||
object creation.
|
||||
|
||||
By default, sets editable=False, default=datetime.now.
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('editable', False)
|
||||
kwargs.setdefault('default', now)
|
||||
super(AutoCreatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class AutoLastModifiedField(AutoCreatedField):
|
||||
"""
|
||||
A DateTimeField that updates itself on each save() of the model.
|
||||
|
||||
By default, sets editable=False and default=datetime.now.
|
||||
|
||||
"""
|
||||
def pre_save(self, model_instance, add):
|
||||
value = now()
|
||||
setattr(model_instance, self.attname, value)
|
||||
return value
|
||||
|
||||
|
||||
class StatusField(models.CharField):
|
||||
"""
|
||||
A CharField that looks for a ``STATUS`` class-attribute and
|
||||
automatically uses that as ``choices``. The first option in
|
||||
``STATUS`` is set as the default.
|
||||
|
||||
Also has a default max_length so you don't have to worry about
|
||||
setting that.
|
||||
|
||||
Also features a ``no_check_for_status`` argument to make sure
|
||||
South can handle this field when it freezes a model.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('max_length', 100)
|
||||
self.check_for_status = not kwargs.pop('no_check_for_status', False)
|
||||
self.choices_name = kwargs.pop('choices_name', DEFAULT_CHOICES_NAME)
|
||||
super(StatusField, self).__init__(*args, **kwargs)
|
||||
|
||||
def prepare_class(self, sender, **kwargs):
|
||||
if not sender._meta.abstract and self.check_for_status:
|
||||
assert hasattr(sender, self.choices_name), \
|
||||
"To use StatusField, the model '%s' must have a %s choices class attribute." \
|
||||
% (sender.__name__, self.choices_name)
|
||||
self._choices = getattr(sender, self.choices_name)
|
||||
if django.VERSION >= (1, 9, 0):
|
||||
self.choices = self._choices
|
||||
if not self.has_default():
|
||||
self.default = tuple(getattr(sender, self.choices_name))[0][0] # set first as default
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
models.signals.class_prepared.connect(self.prepare_class, sender=cls)
|
||||
# we don't set the real choices until class_prepared (so we can rely on
|
||||
# the STATUS class attr being available), but we need to set some dummy
|
||||
# choices now so the super method will add the get_FOO_display method
|
||||
self._choices = [(0, 'dummy')]
|
||||
if django.VERSION >= (1, 9, 0):
|
||||
self.choices = self._choices
|
||||
super(StatusField, self).contribute_to_class(cls, name)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(StatusField, self).deconstruct()
|
||||
kwargs['no_check_for_status'] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
class MonitorField(models.DateTimeField):
|
||||
"""
|
||||
A DateTimeField that monitors another field on the same model and
|
||||
sets itself to the current date/time whenever the monitored field
|
||||
changes.
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('default', now)
|
||||
monitor = kwargs.pop('monitor', None)
|
||||
if not monitor:
|
||||
raise TypeError(
|
||||
'%s requires a "monitor" argument' % self.__class__.__name__)
|
||||
self.monitor = monitor
|
||||
when = kwargs.pop('when', None)
|
||||
if when is not None:
|
||||
when = set(when)
|
||||
self.when = when
|
||||
super(MonitorField, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
self.monitor_attname = '_monitor_%s' % name
|
||||
models.signals.post_init.connect(self._save_initial, sender=cls)
|
||||
super(MonitorField, self).contribute_to_class(cls, name)
|
||||
|
||||
def get_monitored_value(self, instance):
|
||||
return getattr(instance, self.monitor)
|
||||
|
||||
def _save_initial(self, sender, instance, **kwargs):
|
||||
setattr(instance, self.monitor_attname,
|
||||
self.get_monitored_value(instance))
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = now()
|
||||
previous = getattr(model_instance, self.monitor_attname, None)
|
||||
current = self.get_monitored_value(model_instance)
|
||||
if previous != current:
|
||||
if self.when is None or current in self.when:
|
||||
setattr(model_instance, self.attname, value)
|
||||
self._save_initial(model_instance.__class__, model_instance)
|
||||
return super(MonitorField, self).pre_save(model_instance, add)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(MonitorField, self).deconstruct()
|
||||
kwargs['monitor'] = self.monitor
|
||||
if self.when is not None:
|
||||
kwargs['when'] = self.when
|
||||
return name, path, args, kwargs
|
||||
|
||||
|
||||
SPLIT_MARKER = getattr(settings, 'SPLIT_MARKER', '<!-- split -->')
|
||||
|
||||
# the number of paragraphs after which to split if no marker
|
||||
SPLIT_DEFAULT_PARAGRAPHS = getattr(settings, 'SPLIT_DEFAULT_PARAGRAPHS', 2)
|
||||
|
||||
_excerpt_field_name = lambda name: '_%s_excerpt' % name
|
||||
|
||||
def get_excerpt(content):
|
||||
excerpt = []
|
||||
default_excerpt = []
|
||||
paras_seen = 0
|
||||
for line in content.splitlines():
|
||||
if not line.strip():
|
||||
paras_seen += 1
|
||||
if paras_seen < SPLIT_DEFAULT_PARAGRAPHS:
|
||||
default_excerpt.append(line)
|
||||
if line.strip() == SPLIT_MARKER:
|
||||
return '\n'.join(excerpt)
|
||||
excerpt.append(line)
|
||||
|
||||
return '\n'.join(default_excerpt)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class SplitText(object):
|
||||
def __init__(self, instance, field_name, excerpt_field_name):
|
||||
# instead of storing actual values store a reference to the instance
|
||||
# along with field names, this makes assignment possible
|
||||
self.instance = instance
|
||||
self.field_name = field_name
|
||||
self.excerpt_field_name = excerpt_field_name
|
||||
|
||||
# content is read/write
|
||||
def _get_content(self):
|
||||
return self.instance.__dict__[self.field_name]
|
||||
def _set_content(self, val):
|
||||
setattr(self.instance, self.field_name, val)
|
||||
content = property(_get_content, _set_content)
|
||||
|
||||
# excerpt is a read only property
|
||||
def _get_excerpt(self):
|
||||
return getattr(self.instance, self.excerpt_field_name)
|
||||
excerpt = property(_get_excerpt)
|
||||
|
||||
# has_more is a boolean property
|
||||
def _get_has_more(self):
|
||||
return self.excerpt.strip() != self.content.strip()
|
||||
has_more = property(_get_has_more)
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
|
||||
class SplitDescriptor(object):
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
self.excerpt_field_name = _excerpt_field_name(self.field.name)
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
raise AttributeError('Can only be accessed via an instance.')
|
||||
content = instance.__dict__[self.field.name]
|
||||
if content is None:
|
||||
return None
|
||||
return SplitText(instance, self.field.name, self.excerpt_field_name)
|
||||
|
||||
def __set__(self, obj, value):
|
||||
if isinstance(value, SplitText):
|
||||
obj.__dict__[self.field.name] = value.content
|
||||
setattr(obj, self.excerpt_field_name, value.excerpt)
|
||||
else:
|
||||
obj.__dict__[self.field.name] = value
|
||||
|
||||
class SplitField(models.TextField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# for South FakeORM compatibility: the frozen version of a
|
||||
# SplitField can't try to add an _excerpt field, because the
|
||||
# _excerpt field itself is frozen as well. See introspection
|
||||
# rules below.
|
||||
self.add_excerpt_field = not kwargs.pop('no_excerpt_field', False)
|
||||
super(SplitField, self).__init__(*args, **kwargs)
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
if self.add_excerpt_field and not cls._meta.abstract:
|
||||
excerpt_field = models.TextField(editable=False)
|
||||
cls.add_to_class(_excerpt_field_name(name), excerpt_field)
|
||||
super(SplitField, self).contribute_to_class(cls, name)
|
||||
setattr(cls, self.name, SplitDescriptor(self))
|
||||
|
||||
def pre_save(self, model_instance, add):
|
||||
value = super(SplitField, self).pre_save(model_instance, add)
|
||||
excerpt = get_excerpt(value.content)
|
||||
setattr(model_instance, _excerpt_field_name(self.attname), excerpt)
|
||||
return value.content
|
||||
|
||||
def value_to_string(self, obj):
|
||||
value = self._get_val_from_obj(obj)
|
||||
return value.content
|
||||
|
||||
def get_prep_value(self, value):
|
||||
try:
|
||||
return value.content
|
||||
except AttributeError:
|
||||
return value
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super(SplitField, self).deconstruct()
|
||||
kwargs['no_excerpt_field'] = True
|
||||
return name, path, args, kwargs
|
||||
|
||||
# allow South to handle these fields smoothly
|
||||
try:
|
||||
from south.modelsinspector import add_introspection_rules
|
||||
# For a normal MarkupField, the add_excerpt_field attribute is
|
||||
# always True, which means no_excerpt_field arg will always be
|
||||
# True in a frozen MarkupField, which is what we want.
|
||||
add_introspection_rules(rules=[
|
||||
(
|
||||
(SplitField,),
|
||||
[],
|
||||
{'no_excerpt_field': ('add_excerpt_field', {})}
|
||||
),
|
||||
(
|
||||
(MonitorField,),
|
||||
[],
|
||||
{'monitor': ('monitor', {})}
|
||||
),
|
||||
(
|
||||
(StatusField,),
|
||||
[],
|
||||
{'no_check_for_status': ('check_for_status', {})}
|
||||
),
|
||||
], patterns=['model_utils\.fields\.'])
|
||||
except ImportError:
|
||||
pass
|
||||
|
Binary file not shown.
|
@ -0,0 +1,53 @@
|
|||
# This file is distributed under the same license as the django-model-utils package.
|
||||
#
|
||||
# Translators:
|
||||
# Philipp Steinhardt <steinhardt@myvision.de>, 2015.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-model-utils\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2015-07-20 10:17-0600\n"
|
||||
"PO-Revision-Date: 2015-07-01 10:12+0200\n"
|
||||
"Last-Translator: Philipp Steinhardt <steinhardt@myvision.de>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: models.py:20
|
||||
msgid "created"
|
||||
msgstr "erstellt"
|
||||
|
||||
#: models.py:21
|
||||
msgid "modified"
|
||||
msgstr "bearbeitet"
|
||||
|
||||
#: models.py:33
|
||||
msgid "start"
|
||||
msgstr "Beginn"
|
||||
|
||||
#: models.py:34
|
||||
msgid "end"
|
||||
msgstr "Ende"
|
||||
|
||||
#: models.py:49
|
||||
msgid "status"
|
||||
msgstr "Status"
|
||||
|
||||
#: models.py:50
|
||||
msgid "status changed"
|
||||
msgstr "Status geändert"
|
||||
|
||||
#: tests/models.py:106 tests/models.py:115 tests/models.py:124
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: tests/models.py:107 tests/models.py:116 tests/models.py:125
|
||||
msgid "deleted"
|
||||
msgstr "gelöscht"
|
||||
|
||||
#: tests/models.py:108 tests/models.py:117 tests/models.py:126
|
||||
msgid "on hold"
|
||||
msgstr "wartend"
|
|
@ -0,0 +1,246 @@
|
|||
from __future__ import unicode_literals
|
||||
import django
|
||||
from django.db import models
|
||||
from django.db.models.fields.related import OneToOneField, OneToOneRel
|
||||
from django.db.models.query import QuerySet
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
try:
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.utils.six import string_types
|
||||
except ImportError: # Django < 1.5
|
||||
from django.db.models.sql.constants import LOOKUP_SEP
|
||||
string_types = (basestring,)
|
||||
|
||||
|
||||
class InheritanceQuerySetMixin(object):
|
||||
def select_subclasses(self, *subclasses):
|
||||
levels = self._get_maximum_depth()
|
||||
calculated_subclasses = self._get_subclasses_recurse(
|
||||
self.model, levels=levels)
|
||||
# if none were passed in, we can just short circuit and select all
|
||||
if not subclasses:
|
||||
subclasses = calculated_subclasses
|
||||
else:
|
||||
verified_subclasses = []
|
||||
for subclass in subclasses:
|
||||
# special case for passing in the same model as the queryset
|
||||
# is bound against. Rather than raise an error later, we know
|
||||
# we can allow this through.
|
||||
if subclass is self.model:
|
||||
continue
|
||||
|
||||
if not isinstance(subclass, string_types):
|
||||
subclass = self._get_ancestors_path(
|
||||
subclass, levels=levels)
|
||||
|
||||
if subclass in calculated_subclasses:
|
||||
verified_subclasses.append(subclass)
|
||||
else:
|
||||
raise ValueError(
|
||||
'%r is not in the discovered subclasses, tried: %s' % (
|
||||
subclass, ', '.join(calculated_subclasses))
|
||||
)
|
||||
subclasses = verified_subclasses
|
||||
|
||||
# workaround https://code.djangoproject.com/ticket/16855
|
||||
previous_select_related = self.query.select_related
|
||||
new_qs = self.select_related(*subclasses)
|
||||
previous_is_dict = isinstance(previous_select_related, dict)
|
||||
new_is_dict = isinstance(new_qs.query.select_related, dict)
|
||||
if previous_is_dict and new_is_dict:
|
||||
new_qs.query.select_related.update(previous_select_related)
|
||||
new_qs.subclasses = subclasses
|
||||
return new_qs
|
||||
|
||||
def _clone(self, klass=None, setup=False, **kwargs):
|
||||
for name in ['subclasses', '_annotated']:
|
||||
if hasattr(self, name):
|
||||
kwargs[name] = getattr(self, name)
|
||||
if django.VERSION < (1, 9):
|
||||
kwargs['klass'] = klass
|
||||
kwargs['setup'] = setup
|
||||
return super(InheritanceQuerySetMixin, self)._clone(**kwargs)
|
||||
|
||||
def annotate(self, *args, **kwargs):
|
||||
qset = super(InheritanceQuerySetMixin, self).annotate(*args, **kwargs)
|
||||
qset._annotated = [a.default_alias for a in args] + list(kwargs.keys())
|
||||
return qset
|
||||
|
||||
def iterator(self):
|
||||
iter = super(InheritanceQuerySetMixin, self).iterator()
|
||||
if getattr(self, 'subclasses', False):
|
||||
extras = tuple(self.query.extra.keys())
|
||||
# sort the subclass names longest first,
|
||||
# so with 'a' and 'a__b' it goes as deep as possible
|
||||
subclasses = sorted(self.subclasses, key=len, reverse=True)
|
||||
for obj in iter:
|
||||
sub_obj = None
|
||||
for s in subclasses:
|
||||
sub_obj = self._get_sub_obj_recurse(obj, s)
|
||||
if sub_obj:
|
||||
break
|
||||
if not sub_obj:
|
||||
sub_obj = obj
|
||||
|
||||
if getattr(self, '_annotated', False):
|
||||
for k in self._annotated:
|
||||
setattr(sub_obj, k, getattr(obj, k))
|
||||
|
||||
for k in extras:
|
||||
setattr(sub_obj, k, getattr(obj, k))
|
||||
|
||||
yield sub_obj
|
||||
else:
|
||||
for obj in iter:
|
||||
yield obj
|
||||
|
||||
def _get_subclasses_recurse(self, model, levels=None):
|
||||
"""
|
||||
Given a Model class, find all related objects, exploring children
|
||||
recursively, returning a `list` of strings representing the
|
||||
relations for select_related
|
||||
"""
|
||||
if django.VERSION < (1, 8):
|
||||
related_objects = model._meta.get_all_related_objects()
|
||||
else:
|
||||
related_objects = [
|
||||
f for f in model._meta.get_fields()
|
||||
if isinstance(f, OneToOneRel)]
|
||||
|
||||
rels = [
|
||||
rel for rel in related_objects
|
||||
if isinstance(rel.field, OneToOneField)
|
||||
and issubclass(rel.field.model, model)
|
||||
and model is not rel.field.model
|
||||
]
|
||||
|
||||
subclasses = []
|
||||
if levels:
|
||||
levels -= 1
|
||||
for rel in rels:
|
||||
if levels or levels is None:
|
||||
for subclass in self._get_subclasses_recurse(
|
||||
rel.field.model, levels=levels):
|
||||
subclasses.append(
|
||||
rel.get_accessor_name() + LOOKUP_SEP + subclass)
|
||||
subclasses.append(rel.get_accessor_name())
|
||||
return subclasses
|
||||
|
||||
def _get_ancestors_path(self, model, levels=None):
|
||||
"""
|
||||
Serves as an opposite to _get_subclasses_recurse, instead walking from
|
||||
the Model class up the Model's ancestry and constructing the desired
|
||||
select_related string backwards.
|
||||
"""
|
||||
if not issubclass(model, self.model):
|
||||
raise ValueError(
|
||||
"%r is not a subclass of %r" % (model, self.model))
|
||||
|
||||
ancestry = []
|
||||
# should be a OneToOneField or None
|
||||
parent_link = model._meta.get_ancestor_link(self.model)
|
||||
if levels:
|
||||
levels -= 1
|
||||
while parent_link is not None:
|
||||
if django.VERSION < (1, 8):
|
||||
related = parent_link.related
|
||||
else:
|
||||
related = parent_link.rel
|
||||
ancestry.insert(0, related.get_accessor_name())
|
||||
if levels or levels is None:
|
||||
if django.VERSION < (1, 8):
|
||||
parent_model = related.parent_model
|
||||
else:
|
||||
parent_model = related.model
|
||||
parent_link = parent_model._meta.get_ancestor_link(
|
||||
self.model)
|
||||
else:
|
||||
parent_link = None
|
||||
return LOOKUP_SEP.join(ancestry)
|
||||
|
||||
def _get_sub_obj_recurse(self, obj, s):
|
||||
rel, _, s = s.partition(LOOKUP_SEP)
|
||||
|
||||
# Django 1.9: If a primitive type gets passed to this recursive function,
|
||||
# return None as non-models are not part of inheritance.
|
||||
if not isinstance(obj, models.Model):
|
||||
return None
|
||||
|
||||
try:
|
||||
node = getattr(obj, rel)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
if s:
|
||||
child = self._get_sub_obj_recurse(node, s)
|
||||
return child
|
||||
else:
|
||||
return node
|
||||
|
||||
def get_subclass(self, *args, **kwargs):
|
||||
return self.select_subclasses().get(*args, **kwargs)
|
||||
|
||||
def _get_maximum_depth(self):
|
||||
"""
|
||||
Under Django versions < 1.6, to avoid triggering
|
||||
https://code.djangoproject.com/ticket/16572 we can only look
|
||||
as far as children.
|
||||
"""
|
||||
levels = None
|
||||
if django.VERSION < (1, 6, 0):
|
||||
levels = 1
|
||||
return levels
|
||||
|
||||
|
||||
class InheritanceManagerMixin(object):
|
||||
use_for_related_fields = True
|
||||
|
||||
def get_queryset(self):
|
||||
return InheritanceQuerySet(self.model)
|
||||
|
||||
get_query_set = get_queryset
|
||||
|
||||
def select_subclasses(self, *subclasses):
|
||||
return self.get_queryset().select_subclasses(*subclasses)
|
||||
|
||||
def get_subclass(self, *args, **kwargs):
|
||||
return self.get_queryset().get_subclass(*args, **kwargs)
|
||||
|
||||
|
||||
class InheritanceQuerySet(InheritanceQuerySetMixin, QuerySet):
|
||||
pass
|
||||
|
||||
|
||||
class InheritanceManager(InheritanceManagerMixin, models.Manager):
|
||||
pass
|
||||
|
||||
|
||||
class QueryManagerMixin(object):
|
||||
use_for_related_fields = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if args:
|
||||
self._q = args[0]
|
||||
else:
|
||||
self._q = models.Q(**kwargs)
|
||||
self._order_by = None
|
||||
super(QueryManagerMixin, self).__init__()
|
||||
|
||||
def order_by(self, *args):
|
||||
self._order_by = args
|
||||
return self
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
qs = super(QueryManagerMixin, self).get_queryset().filter(self._q)
|
||||
except AttributeError:
|
||||
qs = super(QueryManagerMixin, self).get_query_set().filter(self._q)
|
||||
if self._order_by is not None:
|
||||
return qs.order_by(*self._order_by)
|
||||
return qs
|
||||
|
||||
get_query_set = get_queryset
|
||||
|
||||
|
||||
class QueryManager(QueryManagerMixin, models.Manager):
|
||||
pass
|
|
@ -0,0 +1,101 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
if django.VERSION >= (1, 9, 0):
|
||||
from django.db.models.functions import Now
|
||||
now = Now()
|
||||
else:
|
||||
from django.utils.timezone import now
|
||||
|
||||
from model_utils.managers import QueryManager
|
||||
from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
|
||||
StatusField, MonitorField
|
||||
|
||||
|
||||
class TimeStampedModel(models.Model):
|
||||
"""
|
||||
An abstract base class model that provides self-updating
|
||||
``created`` and ``modified`` fields.
|
||||
|
||||
"""
|
||||
created = AutoCreatedField(_('created'))
|
||||
modified = AutoLastModifiedField(_('modified'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class TimeFramedModel(models.Model):
|
||||
"""
|
||||
An abstract base class model that provides ``start``
|
||||
and ``end`` fields to record a timeframe.
|
||||
|
||||
"""
|
||||
start = models.DateTimeField(_('start'), null=True, blank=True)
|
||||
end = models.DateTimeField(_('end'), null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class StatusModel(models.Model):
|
||||
"""
|
||||
An abstract base class model with a ``status`` field that
|
||||
automatically uses a ``STATUS`` class attribute of choices, a
|
||||
``status_changed`` date-time field that records when ``status``
|
||||
was last modified, and an automatically-added manager for each
|
||||
status that returns objects with that status only.
|
||||
|
||||
"""
|
||||
status = StatusField(_('status'))
|
||||
status_changed = MonitorField(_('status changed'), monitor='status')
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def add_status_query_managers(sender, **kwargs):
|
||||
"""
|
||||
Add a Querymanager for each status item dynamically.
|
||||
|
||||
"""
|
||||
if not issubclass(sender, StatusModel):
|
||||
return
|
||||
for value, display in getattr(sender, 'STATUS', ()):
|
||||
if _field_exists(sender, value):
|
||||
raise ImproperlyConfigured(
|
||||
"StatusModel: Model '%s' has a field named '%s' which "
|
||||
"conflicts with a status of the same name."
|
||||
% (sender.__name__, value)
|
||||
)
|
||||
sender.add_to_class(value, QueryManager(status=value))
|
||||
|
||||
|
||||
def add_timeframed_query_manager(sender, **kwargs):
|
||||
"""
|
||||
Add a QueryManager for a specific timeframe.
|
||||
|
||||
"""
|
||||
if not issubclass(sender, TimeFramedModel):
|
||||
return
|
||||
if _field_exists(sender, 'timeframed'):
|
||||
raise ImproperlyConfigured(
|
||||
"Model '%s' has a field named 'timeframed' "
|
||||
"which conflicts with the TimeFramedModel manager."
|
||||
% sender.__name__
|
||||
)
|
||||
sender.add_to_class('timeframed', QueryManager(
|
||||
(models.Q(start__lte=now) | models.Q(start__isnull=True)) &
|
||||
(models.Q(end__gte=now) | models.Q(end__isnull=True))
|
||||
))
|
||||
|
||||
|
||||
models.signals.class_prepared.connect(add_status_query_managers)
|
||||
models.signals.class_prepared.connect(add_timeframed_query_manager)
|
||||
|
||||
|
||||
def _field_exists(model_class, field_name):
|
||||
return field_name in [f.attname for f in model_class._meta.local_fields]
|
|
@ -0,0 +1,43 @@
|
|||
import django
|
||||
from django.db import models
|
||||
from django.utils.six import with_metaclass, string_types
|
||||
|
||||
|
||||
def mutable_from_db(value):
|
||||
if value == '':
|
||||
return None
|
||||
try:
|
||||
if isinstance(value, string_types):
|
||||
return [int(i) for i in value.split(',')]
|
||||
except ValueError:
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
def mutable_to_db(value):
|
||||
if value is None:
|
||||
return ''
|
||||
if isinstance(value, list):
|
||||
value = ','.join((str(i) for i in value))
|
||||
return str(value)
|
||||
|
||||
|
||||
if django.VERSION >= (1, 9, 0):
|
||||
class MutableField(models.TextField):
|
||||
def to_python(self, value):
|
||||
return mutable_from_db(value)
|
||||
|
||||
def from_db_value(self, value, expression, connection, context):
|
||||
return mutable_from_db(value)
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
value = super(MutableField, self).get_db_prep_save(value, connection)
|
||||
return mutable_to_db(value)
|
||||
else:
|
||||
class MutableField(with_metaclass(models.SubfieldBase, models.TextField)):
|
||||
def to_python(self, value):
|
||||
return mutable_from_db(value)
|
||||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
value = mutable_to_db(value)
|
||||
return super(MutableField, self).get_db_prep_save(value, connection)
|
|
@ -0,0 +1,300 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel
|
||||
from model_utils.tracker import FieldTracker, ModelTracker
|
||||
from model_utils.managers import QueryManager, InheritanceManager
|
||||
from model_utils.fields import SplitField, MonitorField, StatusField
|
||||
from model_utils.tests.fields import MutableField
|
||||
from model_utils import Choices
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestRelated(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class InheritanceManagerTestParent(models.Model):
|
||||
# FileField is just a handy descriptor-using field. Refs #6.
|
||||
non_related_field_using_descriptor = models.FileField(upload_to="test")
|
||||
related = models.ForeignKey(
|
||||
InheritanceManagerTestRelated, related_name="imtests", null=True)
|
||||
normal_field = models.TextField()
|
||||
related_self = models.OneToOneField("self", related_name="imtests_self", null=True)
|
||||
objects = InheritanceManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.pk)
|
||||
|
||||
def __str__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__[len('InheritanceManagerTest'):],
|
||||
self.pk,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestChild1(InheritanceManagerTestParent):
|
||||
non_related_field_using_descriptor_2 = models.FileField(upload_to="test")
|
||||
normal_field_2 = models.TextField()
|
||||
objects = InheritanceManager()
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestGrandChild1(InheritanceManagerTestChild1):
|
||||
text_field = models.TextField()
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestGrandChild1_2(InheritanceManagerTestChild1):
|
||||
text_field = models.TextField()
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestChild2(InheritanceManagerTestParent):
|
||||
non_related_field_using_descriptor_2 = models.FileField(upload_to="test")
|
||||
normal_field_2 = models.TextField()
|
||||
|
||||
|
||||
|
||||
class InheritanceManagerTestChild3(InheritanceManagerTestParent):
|
||||
parent_ptr = models.OneToOneField(
|
||||
InheritanceManagerTestParent, related_name='manual_onetoone',
|
||||
parent_link=True)
|
||||
|
||||
|
||||
class TimeStamp(TimeStampedModel):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TimeFrame(TimeFramedModel):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TimeFrameManagerAdded(TimeFramedModel):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Monitored(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
name_changed = MonitorField(monitor="name")
|
||||
|
||||
|
||||
|
||||
class MonitorWhen(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
name_changed = MonitorField(monitor="name", when=["Jose", "Maria"])
|
||||
|
||||
|
||||
|
||||
class MonitorWhenEmpty(models.Model):
|
||||
name = models.CharField(max_length=25)
|
||||
name_changed = MonitorField(monitor="name", when=[])
|
||||
|
||||
|
||||
|
||||
class Status(StatusModel):
|
||||
STATUS = Choices(
|
||||
("active", _("active")),
|
||||
("deleted", _("deleted")),
|
||||
("on_hold", _("on hold")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class StatusPlainTuple(StatusModel):
|
||||
STATUS = (
|
||||
("active", _("active")),
|
||||
("deleted", _("deleted")),
|
||||
("on_hold", _("on hold")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class StatusManagerAdded(StatusModel):
|
||||
STATUS = (
|
||||
("active", _("active")),
|
||||
("deleted", _("deleted")),
|
||||
("on_hold", _("on hold")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
published = models.BooleanField(default=False)
|
||||
confirmed = models.BooleanField(default=False)
|
||||
order = models.IntegerField()
|
||||
|
||||
objects = models.Manager()
|
||||
public = QueryManager(published=True)
|
||||
public_confirmed = QueryManager(models.Q(published=True) &
|
||||
models.Q(confirmed=True))
|
||||
public_reversed = QueryManager(published=True).order_by("-order")
|
||||
|
||||
class Meta:
|
||||
ordering = ("order",)
|
||||
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
title = models.CharField(max_length=50)
|
||||
body = SplitField()
|
||||
|
||||
|
||||
|
||||
class SplitFieldAbstractParent(models.Model):
|
||||
content = SplitField()
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
|
||||
class NoRendered(models.Model):
|
||||
"""
|
||||
Test that the no_excerpt_field keyword arg works. This arg should
|
||||
never be used except by the South model-freezing.
|
||||
|
||||
"""
|
||||
body = SplitField(no_excerpt_field=True)
|
||||
|
||||
|
||||
|
||||
class AuthorMixin(object):
|
||||
def by_author(self, name):
|
||||
return self.filter(author=name)
|
||||
|
||||
|
||||
|
||||
class PublishedMixin(object):
|
||||
def published(self):
|
||||
return self.filter(published=True)
|
||||
|
||||
|
||||
|
||||
def unpublished(self):
|
||||
return self.filter(published=False)
|
||||
|
||||
|
||||
|
||||
class ByAuthorQuerySet(models.query.QuerySet, AuthorMixin):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class FeaturedManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
kwargs = {}
|
||||
if hasattr(self, "_db"):
|
||||
kwargs["using"] = self._db
|
||||
return ByAuthorQuerySet(self.model, **kwargs).filter(feature=True)
|
||||
|
||||
get_query_set = get_queryset
|
||||
|
||||
|
||||
class Tracked(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
number = models.IntegerField()
|
||||
mutable = MutableField(default=None)
|
||||
|
||||
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 = 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 InheritedTracked(Tracked):
|
||||
name2 = models.CharField(max_length=20)
|
||||
|
||||
|
||||
class InheritedTrackedFK(TrackedFK):
|
||||
custom_tracker = FieldTracker(fields=['fk_id'])
|
||||
custom_tracker_without_id = FieldTracker(fields=['fk'])
|
||||
|
||||
|
||||
class ModelTracked(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
number = models.IntegerField()
|
||||
mutable = MutableField(default=None)
|
||||
|
||||
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'])
|
||||
|
||||
class InheritedModelTracked(ModelTracked):
|
||||
name2 = models.CharField(max_length=20)
|
||||
|
||||
|
||||
class StatusFieldDefaultFilled(models.Model):
|
||||
STATUS = Choices((0, "no", "No"), (1, "yes", "Yes"))
|
||||
status = StatusField(default=STATUS.yes)
|
||||
|
||||
|
||||
class StatusFieldDefaultNotFilled(models.Model):
|
||||
STATUS = Choices((0, "no", "No"), (1, "yes", "Yes"))
|
||||
status = StatusField()
|
||||
|
||||
|
||||
class StatusFieldChoicesName(models.Model):
|
||||
NAMED_STATUS = Choices((0, "no", "No"), (1, "yes", "Yes"))
|
||||
status = StatusField(choices_name='NAMED_STATUS')
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,197 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import django
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import models
|
||||
from django.db.models.query_utils import DeferredAttribute
|
||||
|
||||
|
||||
class FieldInstanceTracker(object):
|
||||
def __init__(self, instance, fields, field_map):
|
||||
self.instance = instance
|
||||
self.fields = fields
|
||||
self.field_map = field_map
|
||||
self.init_deferred_fields()
|
||||
|
||||
def get_field_value(self, field):
|
||||
return getattr(self.instance, self.field_map[field])
|
||||
|
||||
def set_saved_fields(self, fields=None):
|
||||
if not self.instance.pk:
|
||||
self.saved_data = {}
|
||||
elif fields is None:
|
||||
self.saved_data = self.current()
|
||||
else:
|
||||
self.saved_data.update(**self.current(fields=fields))
|
||||
|
||||
# preventing mutable fields side effects
|
||||
for field, field_value in self.saved_data.items():
|
||||
self.saved_data[field] = deepcopy(field_value)
|
||||
|
||||
def current(self, fields=None):
|
||||
"""Returns dict of current values for all tracked fields"""
|
||||
if fields is None:
|
||||
if self.instance._deferred_fields:
|
||||
fields = [
|
||||
field for field in self.fields
|
||||
if field not in self.instance._deferred_fields
|
||||
]
|
||||
else:
|
||||
fields = self.fields
|
||||
|
||||
return dict((f, self.get_field_value(f)) for f in fields)
|
||||
|
||||
def has_changed(self, field):
|
||||
"""Returns ``True`` if field has changed from currently saved value"""
|
||||
if field in self.fields:
|
||||
return self.previous(field) != self.get_field_value(field)
|
||||
else:
|
||||
raise FieldError('field "%s" not tracked' % field)
|
||||
|
||||
def previous(self, field):
|
||||
"""Returns currently saved value of given field"""
|
||||
return self.saved_data.get(field)
|
||||
|
||||
def changed(self):
|
||||
"""Returns dict of fields that changed since save (with old values)"""
|
||||
return dict(
|
||||
(field, self.previous(field))
|
||||
for field in self.fields
|
||||
if self.has_changed(field)
|
||||
)
|
||||
|
||||
def init_deferred_fields(self):
|
||||
self.instance._deferred_fields = set()
|
||||
if hasattr(self.instance, '_deferred') and not self.instance._deferred:
|
||||
return
|
||||
|
||||
class DeferredAttributeTracker(DeferredAttribute):
|
||||
def __get__(field, instance, owner):
|
||||
if instance is None:
|
||||
return field
|
||||
data = instance.__dict__
|
||||
if data.get(field.field_name, field) is field:
|
||||
instance._deferred_fields.remove(field.field_name)
|
||||
value = super(DeferredAttributeTracker, field).__get__(
|
||||
instance, owner)
|
||||
self.saved_data[field.field_name] = deepcopy(value)
|
||||
return data[field.field_name]
|
||||
|
||||
if django.VERSION >= (1, 8):
|
||||
self.instance._deferred_fields = self.instance.get_deferred_fields()
|
||||
for field in self.instance._deferred_fields:
|
||||
if django.VERSION >= (1, 10):
|
||||
field_obj = getattr(self.instance.__class__, field)
|
||||
else:
|
||||
field_obj = self.instance.__class__.__dict__.get(field)
|
||||
field_tracker = DeferredAttributeTracker(
|
||||
field_obj.field_name, None)
|
||||
setattr(self.instance.__class__, field, field_tracker)
|
||||
else:
|
||||
for field in self.fields:
|
||||
field_obj = self.instance.__class__.__dict__.get(field)
|
||||
if isinstance(field_obj, DeferredAttribute):
|
||||
self.instance._deferred_fields.add(field)
|
||||
|
||||
# Django 1.4
|
||||
if django.VERSION >= (1, 5):
|
||||
model = None
|
||||
else:
|
||||
model = field_obj.model_ref()
|
||||
|
||||
field_tracker = DeferredAttributeTracker(
|
||||
field_obj.field_name, model)
|
||||
setattr(self.instance.__class__, field, field_tracker)
|
||||
|
||||
|
||||
class FieldTracker(object):
|
||||
|
||||
tracker_class = FieldInstanceTracker
|
||||
|
||||
def __init__(self, fields=None):
|
||||
self.fields = fields
|
||||
|
||||
def get_field_map(self, cls):
|
||||
"""Returns dict mapping fields names to model attribute names"""
|
||||
field_map = dict((field, field) for field in self.fields)
|
||||
all_fields = dict((f.name, f.attname) for f in cls._meta.fields)
|
||||
field_map.update(**dict((k, v) for (k, v) in all_fields.items()
|
||||
if k in field_map))
|
||||
return field_map
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
self.name = name
|
||||
self.attname = '_%s' % name
|
||||
models.signals.class_prepared.connect(self.finalize_class, sender=cls)
|
||||
|
||||
def finalize_class(self, sender, **kwargs):
|
||||
if self.fields is None:
|
||||
self.fields = (field.attname for field in sender._meta.fields)
|
||||
self.fields = set(self.fields)
|
||||
self.field_map = self.get_field_map(sender)
|
||||
models.signals.post_init.connect(self.initialize_tracker)
|
||||
self.model_class = sender
|
||||
setattr(sender, self.name, self)
|
||||
|
||||
def initialize_tracker(self, sender, instance, **kwargs):
|
||||
if not isinstance(instance, self.model_class):
|
||||
return # Only init instances of given model (including children)
|
||||
tracker = self.tracker_class(instance, self.fields, self.field_map)
|
||||
setattr(instance, self.attname, tracker)
|
||||
tracker.set_saved_fields()
|
||||
self.patch_save(instance)
|
||||
|
||||
def patch_save(self, instance):
|
||||
original_save = instance.save
|
||||
def save(**kwargs):
|
||||
ret = original_save(**kwargs)
|
||||
update_fields = kwargs.get('update_fields')
|
||||
if not update_fields and update_fields is not None: # () or []
|
||||
fields = update_fields
|
||||
elif update_fields is None:
|
||||
fields = None
|
||||
else:
|
||||
fields = (
|
||||
field for field in update_fields if
|
||||
field in self.fields
|
||||
)
|
||||
getattr(instance, self.attname).set_saved_fields(
|
||||
fields=fields
|
||||
)
|
||||
return ret
|
||||
instance.save = save
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
else:
|
||||
return getattr(instance, self.attname)
|
||||
|
||||
|
||||
class ModelInstanceTracker(FieldInstanceTracker):
|
||||
|
||||
def has_changed(self, field):
|
||||
"""Returns ``True`` if field has changed from currently saved value"""
|
||||
if not self.instance.pk:
|
||||
return True
|
||||
elif field in self.saved_data:
|
||||
return self.previous(field) != self.get_field_value(field)
|
||||
else:
|
||||
raise FieldError('field "%s" not tracked' % field)
|
||||
|
||||
def changed(self):
|
||||
"""Returns dict of fields that changed since save (with old values)"""
|
||||
if not self.instance.pk:
|
||||
return {}
|
||||
saved = self.saved_data.items()
|
||||
current = self.current()
|
||||
return dict((k, v) for k, v in saved if v != current[k])
|
||||
|
||||
|
||||
class ModelTracker(FieldTracker):
|
||||
tracker_class = ModelInstanceTracker
|
||||
|
||||
def get_field_map(self, cls):
|
||||
return dict((field, field) for field in self.fields)
|
|
@ -0,0 +1,3 @@
|
|||
tox
|
||||
sphinx
|
||||
twine
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os, sys
|
||||
|
||||
from django.conf import settings
|
||||
import django
|
||||
|
||||
|
||||
DEFAULT_SETTINGS = dict(
|
||||
INSTALLED_APPS=(
|
||||
'model_utils',
|
||||
'model_utils.tests',
|
||||
),
|
||||
DATABASES={
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3"
|
||||
}
|
||||
},
|
||||
SILENCED_SYSTEM_CHECKS=["1_7.W001"],
|
||||
)
|
||||
|
||||
|
||||
def runtests():
|
||||
if not settings.configured:
|
||||
settings.configure(**DEFAULT_SETTINGS)
|
||||
|
||||
# Compatibility with Django 1.7's stricter initialization
|
||||
if hasattr(django, 'setup'):
|
||||
django.setup()
|
||||
|
||||
parent = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, parent)
|
||||
|
||||
try:
|
||||
from django.test.runner import DiscoverRunner
|
||||
runner_class = DiscoverRunner
|
||||
test_args = ['model_utils.tests']
|
||||
except ImportError:
|
||||
from django.test.simple import DjangoTestSuiteRunner
|
||||
runner_class = DjangoTestSuiteRunner
|
||||
test_args = ['tests']
|
||||
|
||||
failures = runner_class(
|
||||
verbosity=1, interactive=True, failfast=False).run_tests(test_args)
|
||||
sys.exit(failures)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
runtests()
|
|
@ -0,0 +1,7 @@
|
|||
[build_sphinx]
|
||||
source-dir = docs/
|
||||
build-dir = docs/_build
|
||||
all_files = 1
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
|
@ -0,0 +1,57 @@
|
|||
import os
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
def long_desc(root_path):
|
||||
FILES = ['README.rst', 'CHANGES.rst']
|
||||
for filename in FILES:
|
||||
filepath = os.path.realpath(os.path.join(root_path, filename))
|
||||
if os.path.isfile(filepath):
|
||||
with open(filepath, mode='r') as f:
|
||||
yield f.read()
|
||||
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
long_description = "\n\n".join(long_desc(HERE))
|
||||
|
||||
|
||||
def get_version(root_path):
|
||||
with open(os.path.join(root_path, 'model_utils', '__init__.py')) as f:
|
||||
for line in f:
|
||||
if line.startswith('__version__ ='):
|
||||
return line.split('=')[1].strip().strip('"\'')
|
||||
|
||||
setup(
|
||||
name='django-model-utils',
|
||||
version=get_version(HERE),
|
||||
description='Django model mixins and utilities',
|
||||
long_description=long_description,
|
||||
author='Carl Meyer',
|
||||
author_email='carl@oddbird.net',
|
||||
url='https://github.com/carljm/django-model-utils/',
|
||||
packages=find_packages(),
|
||||
install_requires=['Django>=1.4.2'],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Framework :: Django',
|
||||
],
|
||||
zip_safe=False,
|
||||
tests_require=["Django>=1.4.2"],
|
||||
test_suite='runtests.runtests',
|
||||
package_data={
|
||||
'model_utils': [
|
||||
'locale/*/LC_MESSAGES/django.po','locale/*/LC_MESSAGES/django.mo'
|
||||
],
|
||||
},
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py26-django{14,15,16},
|
||||
py27-django{14,19,110,_trunk}, py27-django15_nosouth,
|
||||
py{27,33}-django{15,16,17,18},
|
||||
py34-django{17,18,19,110,_trunk},
|
||||
py35-django{18,19,110,_trunk},
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
|
||||
deps =
|
||||
coverage == 3.6
|
||||
django14: Django>=1.4,<1.5
|
||||
django15{,_nosouth}: Django>=1.5,<1.6
|
||||
django16: Django>=1.6,<1.7
|
||||
django17: Django>=1.7,<1.8
|
||||
django18: Django>=1.8,<1.9
|
||||
django19: Django>=1.9,<1.10
|
||||
django110: Django>=1.10,<1.11
|
||||
django_trunk: https://github.com/django/django/tarball/master
|
||||
django{14,15,16}: South==1.0.2
|
||||
|
||||
commands = coverage run -a setup.py test
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
import django
|
||||
|
||||
|
||||
DEFAULT_SETTINGS = dict(
|
||||
INSTALLED_APPS=(
|
||||
'model_utils',
|
||||
'model_utils.tests',
|
||||
),
|
||||
DATABASES={
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3"
|
||||
}
|
||||
},
|
||||
SILENCED_SYSTEM_CHECKS=["1_7.W001"],
|
||||
)
|
||||
|
||||
|
||||
def run(command):
|
||||
if not settings.configured:
|
||||
settings.configure(**DEFAULT_SETTINGS)
|
||||
|
||||
# Compatibility with Django 1.7's stricter initialization
|
||||
if hasattr(django, 'setup'):
|
||||
django.setup()
|
||||
|
||||
parent = os.path.dirname(os.path.abspath(__file__))
|
||||
appdir = os.path.join(parent, 'model_utils')
|
||||
os.chdir(appdir)
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
call_command('%smessages' % command)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if (len(sys.argv)) < 2 or (sys.argv[1] not in {'make', 'compile'}):
|
||||
print("Run `translations.py make` or `translations.py compile`.")
|
||||
sys.exit(1)
|
||||
run(sys.argv[1])
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Updates .travis.yml envs based on tox.ini configuration.
|
||||
|
||||
# Removing old environment list
|
||||
cp ./.travis.yml ./.travis.yml.bak
|
||||
cat ./.travis.yml.bak | grep -v "^ - TOXENV=" > ./.travis.yml
|
||||
|
||||
# Inserting envs based on list generated by tox
|
||||
for env_name in $(tox --listenvs | sort -r); do
|
||||
sed -i "/^env:$/a\
|
||||
\ \ - TOXENV=${env_name}" ./.travis.yml;
|
||||
done
|
Loading…
Reference in New Issue