Commit cc0561ad authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 1.3.1

parent 91acc38d
......@@ -4,4 +4,5 @@ HGREV
.coverage
.tox/
Django-*.egg
*.pyc
\ No newline at end of file
*.pyc
htmlcov/
repo: e9d694df3aebe0c1931e3e24f28170d9e88bda16
node: 92792fb14a51b580e5cc8991e815f3b3b57a6204
branch: default
tag: 1.1.0
......@@ -4,3 +4,4 @@
^\.coverage$
^\.tox/
^Django.*\.egg$
^htmlcov/
......@@ -3,3 +3,4 @@ b5efc435bb7e21b0d7ba422d28d174ccca3b3322 0.2.0
1e6f730f8c3a648c9fb70844a68fcfa663608600 0.4.0
004dbee634cb661c52acac034063989e521c4bb8 0.5.0
bd164041e5fabd64de19c38fefe9af9237a2a59e 1.0.0
92792fb14a51b580e5cc8991e815f3b3b57a6204 1.1.0
language: python
python:
- "2.6"
- "2.7"
env:
- DJANGO=Django==1.2.7 SOUTH=1
- DJANGO=Django==1.3.7 SOUTH=1
- DJANGO=Django==1.4.5 SOUTH=1
- DJANGO=Django==1.4.5 SOUTH=1
- DJANGO=Django==1.5 SOUTH=1
- DJANGO=https://github.com/django/django/tarball/master SOUTH=1
- DJANGO=Django==1.4.5 SOUTH=0
install:
- pip install $DJANGO --use-mirrors
- pip install coverage coveralls --use-mirrors
- sh -c "if [ '$SOUTH' = '1' ]; then pip install South==0.7.6; fi"
script: coverage run -a --branch --include="model_utils/*" --omit="model_utils/tests/*" setup.py test
after_success: coveralls
Alejandro Varas <alej0varas@gmail.com>
Carl Meyer <carl@dirtcircle.com>
Donald Stufft <donald.stufft@gmail.com>
Facundo Gaich <facugaich@gmail.com>
Felipe Prenholato <philipe.rp@gmail.com>
Gregor Müllegger <gregor@muellegger.de>
James Oakley <jfunk@funktronics.ca>
Jannis Leidel <jannis@leidel.info>
Javier García Sogo <jgsogo@gmail.com>
Jeff Elmore <jeffelmore.org>
ivirabyan
Paul McLanahan <paul@mclanahan.net>
Ryan Kaskel
Rinat Shigapov <rinatshigapov@gmail.com>
Ryan Kaskel <dev@ryankaskel.com>
Simon Meers <simon@simonmeers.com>
sayane
Trey Hunner <trey@treyhunner.com>
zyegfryed
CHANGES
=======
tip (unreleased)
----------------
1.3.1 (2013.04.11)
------------------
- Added explicit default to ``BooleanField`` in tests, for Django trunk
compatibility.
- Fix intermittent ``StatusField`` bug. Fixes GH-29.
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 García Sogo.
1.1.0 (2012.04.13)
------------------
......
Copyright (c) 2009-2011, Carl Meyer and contributors
Copyright (c) 2009-2013, Carl Meyer and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without
......
......@@ -4,4 +4,3 @@ include LICENSE.txt
include MANIFEST.in
include README.rst
include TODO.rst
include HGREV
......@@ -2,6 +2,11 @@
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
Django model mixins and utilities.
Installation
......@@ -24,11 +29,28 @@ your ``INSTALLED_APPS`` setting.
Dependencies
------------
Most of ``django-model-utils`` works with `Django`_ 1.1 or later.
`InheritanceManager`_ and `SplitField`_ require Django 1.2 or later.
``django-model-utils`` is tested with `Django`_ 1.2 and later on Python 2.6 and 2.7.
.. _Django: http://www.djangoproject.com/
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
(Until January 2013 django-model-utils primary development was hosted at
`BitBucket`_; the issue tracker there will remain open until all issues and
pull requests tracked in it are closed, but all new issues should be filed at
GitHub.)
.. _BitBucket: https://bitbucket.org/carljm/django-model-utils/overview
Choices
=======
......@@ -95,6 +117,12 @@ default value to the first item in the ``STATUS`` choices::
(The ``STATUS`` class attribute does not have to be a `Choices`_
instance, it can be an ordinary list of two-tuples).
``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
============
......@@ -146,7 +174,7 @@ object has three attributes:
``excerpt``:
The excerpt of ``content`` (read-only).
``has_more``:
True if the excerpt and content are the same, False otherwise.
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
......@@ -274,24 +302,19 @@ an ``InheritanceManager`` behaves identically to a normal ``Manager``; so
it's safe to use as your default manager for the model.
.. note::
``InheritanceManager`` currently only supports a single level of model
inheritance; it won't work for grandchild models.
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.
.. note::
The implementation of ``InheritanceManager`` uses ``select_related``
internally. Due to `Django bug #16855`_, this currently means that it
will override any previous ``select_related`` calls on the ``QuerySet``.
.. note::
``InheritanceManager`` requires Django 1.2 or later. Previous versions of
django-model-utils included ``InheritanceCastModel``, an alternative (and
inferior) approach to this problem that is Django 1.1
compatible. ``InheritanceCastModel`` will remain in django-model-utils
until support for Django 1.1 is removed, but it is no longer documented and
its use in new code is discouraged.
.. _contributed by Jeff Elmore: http://jeffelmore.org/2010/11/11/automatic-downcasting-of-inherited-models-in-django/
.. _Django bug #16855: https://code.djangoproject.com/ticket/16855
.. _Django bug #16572: https://code.djangoproject.com/ticket/16572
TimeStampedModel
......@@ -351,32 +374,107 @@ directly on the manager::
from datetime import datetime
from django.db import models
from django.db.models.query import QuerySet
from model_utils.managers import PassThroughManager
class PostQuerySet(QuerySet):
def by_author(self, user):
return self.filter(user=user)
def published(self):
return self.filter(published__lte=datetime.now())
def unpublished(self):
return self.filter(published__gte=datetime.now())
class Post(models.Model):
user = models.ForeignKey(User)
published = models.DateTimeField()
objects = PassThroughManager.for_queryset_class(PostQuerySet)()
Post.objects.published()
Post.objects.by_author(user=request.user).unpublished()
.. note::
Previous versions of django-model-utils included ``manager_from``, a
function that solved the same problem as ``PassThroughManager``. The
``manager_from`` approach created dynamic ``QuerySet`` subclasses on the
fly, which broke pickling of those querysets. For this reason,
``PassThroughManager`` is recommended instead.
ModelTracker
============
A ``ModelTracker`` can be added to a model to track changes in model fields. A
``ModelTracker`` allows querying for field changes since a model instance was
last saved. An example of applying ``ModelTracker`` to a model::
from django.db import models
from model_utils import ModelTracker
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
tracker = ModelTracker()
Accessing a model 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::
>>> 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::
>>> a = Post.objects.create(title='First Post')
>>> a.title = 'Welcome'
>>> a.tracker.has_changed('title')
True
>>> a.tracker.has_changed('body')
False
Returns ``True`` if the model instance hasn't been saved yet.
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::
>>> a = Post.objects.create(title='First Post')
>>> a.title = 'Welcome'
>>> a.body = 'First post!'
>>> a.tracker.changed()
{'title': 'First Post', 'body': ''}
Returns ``{}`` if the model instance hasn't been saved yet.
Tracking specific fields
------------------------
A fields parameter can be given to ``ModelTracker`` to limit model tracking to
the specific fields::
from django.db import models
from model_utils import ModelTracker
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
title_tracker = ModelTracker(fields=['title'])
An example using the model specified above::
>>> a = Post.objects.create(title='First Post')
>>> a.body = 'First post!'
>>> a.title_tracker.changed()
{}
TODO
====
* Add support for multiple levels of inheritance to ``InheritanceManager``.
* Switch to proper test skips once Django 1.3 is minimum supported.
from django import VERSION
if VERSION < (1, 2):
import warnings
warnings.warn(
"Django 1.1 support in django-model-utils is pending deprecation.",
PendingDeprecationWarning)
class ChoiceEnum(object):
"""
DEPRECATED: Use ``Choices`` (below) instead. This class has less
flexibility for human-readable display, and greater potential for
surprising data corruption if new choices are inserted in the
middle of the list. Automatic assignment of numeric IDs is not
such a great idea after all.
A class to encapsulate handy functionality for lists of choices
for a Django model field.
Accepts verbose choice names as arguments, and automatically
assigns numeric keys to them. When iterated over, behaves as the
standard Django choices list of two-tuples.
Attribute access allows conversion of verbose choice name to
choice key, dictionary access the reverse.
Example:
>>> STATUS = ChoiceEnum('DRAFT', 'PUBLISHED')
>>> STATUS.DRAFT
0
>>> STATUS[1]
'PUBLISHED'
>>> tuple(STATUS)
((0, 'DRAFT'), (1, 'PUBLISHED'))
"""
def __init__(self, *choices):
import warnings
warnings.warn("ChoiceEnum is deprecated, use Choices instead.",
DeprecationWarning)
self._choices = tuple(enumerate(choices))
self._choice_dict = dict(self._choices)
self._reverse_dict = dict(((i[1], i[0]) for i in self._choices))
def __iter__(self):
return iter(self._choices)
def __getattr__(self, attname):
try:
return self._reverse_dict[attname]
except KeyError:
raise AttributeError(attname)
def __getitem__(self, key):
return self._choice_dict[key]
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(("'%s'" % i[1] for i in self._choices)))
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.)
"""
def __init__(self, *choices):
self._full = []
self._choices = []
self._choice_dict = {}
for choice in self.equalize(choices):
self._full.append(choice)
self._choices.append((choice[0], choice[2]))
self._choice_dict[choice[1]] = choice[0]
def equalize(self, choices):
for choice in choices:
if isinstance(choice, (list, tuple)):
if len(choice) == 3:
yield choice
elif len(choice) == 2:
yield (choice[0], choice[0], choice[1])
else:
raise ValueError("Choices can't handle a list/tuple of length %s, only 2 or 3"
% len(choice))
else:
yield (choice, choice, choice)
def __iter__(self):
return iter(self._choices)
def __getattr__(self, attname):
try:
return self._choice_dict[attname]
except KeyError:
raise AttributeError(attname)
def __getitem__(self, index):
return self._choices[index]
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(("%s" % str(i) for i in self._full)))
from .choices import Choices
from .tracker import ModelTracker
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.)
"""
def __init__(self, *choices):
self._full = []
self._choices = []
self._choice_dict = {}
for choice in self.equalize(choices):
self._full.append(choice)
self._choices.append((choice[0], choice[2]))
self._choice_dict[choice[1]] = choice[0]
def equalize(self, choices):
for choice in choices:
if isinstance(choice, (list, tuple)):
if len(choice) == 3:
yield choice
elif len(choice) == 2:
yield (choice[0], choice[0], choice[1])
else:
raise ValueError("Choices can't handle a list/tuple of length %s, only 2 or 3"
% len(choice))
else:
yield (choice, choice, choice)
def __len__(self):
return len(self._choices)
def __iter__(self):
return iter(self._choices)
def __getattr__(self, attname):
try:
return self._choice_dict[attname]
except KeyError:
raise AttributeError(attname)
def __getitem__(self, index):
return self._choices[index]
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join(("%s" % str(i) for i in self._full)))
......@@ -3,8 +3,6 @@ from datetime import datetime
from django.db import models
from django.conf import settings
from model_utils import Choices
try:
from django.utils.timezone import now as now
......@@ -56,13 +54,17 @@ class StatusField(models.CharField):
self.check_for_status = not kwargs.pop('no_check_for_status', False)
super(StatusField, self).__init__(*args, **kwargs)
def contribute_to_class(self, cls, name):
if not cls._meta.abstract and self.check_for_status:
assert hasattr(cls, 'STATUS'), \
def prepare_class(self, sender, **kwargs):
if not sender._meta.abstract and self.check_for_status:
assert hasattr(sender, 'STATUS'), \
"To use StatusField, the model '%s' must have a STATUS choices class attribute." \
% cls.__name__
setattr(self, '_choices', cls.STATUS)
setattr(self, 'default', tuple(cls.STATUS)[0][0]) # sets first as default
% sender.__name__
self._choices = sender.STATUS
if not self.has_default():
self.default = tuple(sender.STATUS)[0][0] # set first as default
def contribute_to_class(self, cls, name):
models.signals.class_prepared.connect(self.prepare_class, sender=cls)
super(StatusField, self).contribute_to_class(cls, name)
......
This diff is collapsed.
import warnings
from datetime import datetime
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from django.db.models.fields import FieldDoesNotExist
from django.core.exceptions import ImproperlyConfigured
from model_utils.managers import manager_from, InheritanceCastMixin, \
QueryManager
from model_utils.managers import QueryManager
from model_utils.fields import AutoCreatedField, AutoLastModifiedField, \
StatusField, MonitorField
......@@ -19,42 +15,6 @@ except ImportError:
now = datetime.now
class InheritanceCastModel(models.Model):
"""
An abstract base class that provides a ``real_type`` FK to ContentType.
For use in trees of inherited models, to be able to downcast
parent instances to their child types.
Pending deprecation; use InheritanceManager instead.
"""
real_type = models.ForeignKey(ContentType, editable=False, null=True)
objects = manager_from(InheritanceCastMixin)
def __init__(self, *args, **kwargs):
warnings.warn(
"InheritanceCastModel is pending deprecation. "
"Use InheritanceManager instead.",
PendingDeprecationWarning,
stacklevel=2)
super(InheritanceCastModel, self).__init__(*args, **kwargs)
def save(self, *args, **kwargs):
if not self.id:
self.real_type = self._get_real_type()
super(InheritanceCastModel, self).save(*args, **kwargs)
def _get_real_type(self):
return ContentType.objects.get_for_model(type(self))
def cast(self):
return self.real_type.get_object_for_this_type(pk=self.pk)
class Meta:
abstract = True
class TimeStampedModel(models.Model):
"""
......
from django.db import models
from django.utils.translation import ugettext_lazy as _
from model_utils.models import InheritanceCastModel, TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.managers import QueryManager, manager_from, InheritanceManager, PassThroughManager
from model_utils.fields import SplitField, MonitorField
from model_utils.models import TimeStampedModel, StatusModel, TimeFramedModel
from model_utils.tracker import ModelTracker
from model_utils.managers import QueryManager, InheritanceManager, PassThroughManager
from model_utils.fields import SplitField, MonitorField, StatusField
from model_utils import Choices
class InheritParent(InheritanceCastModel):
non_related_field_using_descriptor = models.FileField(upload_to="test")
normal_field = models.TextField()
pass
class InheritChild(InheritParent):
non_related_field_using_descriptor_2 = models.FileField(upload_to="test")
normal_field_2 = models.TextField()
pass
class InheritChild2(InheritParent):
non_related_field_using_descriptor_3 = models.FileField(upload_to="test")
normal_field_3 = models.TextField()
pass
class InheritanceManagerTestRelated(models.Model):
pass
......@@ -50,6 +30,9 @@ class InheritanceManagerTestChild1(InheritanceManagerTestParent):
pass
class InheritanceManagerTestGrandChild1(InheritanceManagerTestChild1):
text_field = models.TextField()
class InheritanceManagerTestChild2(InheritanceManagerTestParent):
non_related_field_using_descriptor_2 = models.FileField(upload_to="test")
......@@ -178,19 +161,6 @@ class FeaturedManager(models.Manager):
class Entry(models.Model):
author = models.CharField(max_length=20)
published = models.BooleanField()
feature = models.BooleanField(default=False)
objects = manager_from(AuthorMixin, PublishedMixin, unpublished)
broken = manager_from(PublishedMixin, manager_cls=FeaturedManager)
featured = manager_from(PublishedMixin,
manager_cls=FeaturedManager,
queryset_cls=ByAuthorQuerySet)
class DudeQuerySet(models.query.QuerySet):
def abiding(self):
return self.filter(abides=True)
......@@ -221,7 +191,7 @@ class AbidingManager(PassThroughManager):
class Dude(models.Model):
abides = models.BooleanField(default=True)
name = models.CharField(max_length=20)
has_rug = models.BooleanField()
has_rug = models.BooleanField(default=False)
objects = PassThroughManager(DudeQuerySet)
abiders = AbidingManager()
......@@ -249,3 +219,35 @@ class Spot(models.Model):
owner = models.ForeignKey(Dude, related_name='spots_owned')
objects = PassThroughManager.for_queryset_class(SpotQuerySet)()
class Tracked(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()
tracker = ModelTracker()
class TrackedNotDefault(models.Model):
name = models.CharField(max_length=20)
number = models.IntegerField()
name_tracker = ModelTracker(fields=['name'])