--- a/web/lib/django/db/models/fields/related.py Wed May 19 17:43:59 2010 +0200
+++ b/web/lib/django/db/models/fields/related.py Tue May 25 02:43:45 2010 +0200
@@ -1,20 +1,18 @@
-from django.db import connection, transaction
+from django.conf import settings
+from django.db import connection, router, transaction
from django.db.backends import util
from django.db.models import signals, get_model
-from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist
+from django.db.models.fields import (AutoField, Field, IntegerField,
+ PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist)
from django.db.models.related import RelatedObject
from django.db.models.query import QuerySet
from django.db.models.query_utils import QueryWrapper
from django.utils.encoding import smart_unicode
-from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _
+from django.utils.translation import ugettext_lazy as _, string_concat, ungettext, ugettext
from django.utils.functional import curry
from django.core import exceptions
from django import forms
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
@@ -58,6 +56,10 @@
# If we can't split, assume a model in current app
app_label = cls._meta.app_label
model_name = relation
+ except AttributeError:
+ # If it doesn't have a split it's actually a model class
+ app_label = relation._meta.app_label
+ model_name = relation._meta.object_name
# Try to look up the related model, and if it's already loaded resolve the
# string right away. If get_model returns None, it means that the related
@@ -86,17 +88,20 @@
def contribute_to_class(self, cls, name):
sup = super(RelatedField, self)
- # Add an accessor to allow easy determination of the related query path for this field
- self.related_query_name = curry(self._get_related_query_name, cls._meta)
+ # Store the opts for related_query_name()
+ self.opts = cls._meta
if hasattr(sup, 'contribute_to_class'):
sup.contribute_to_class(cls, name)
if not cls._meta.abstract and self.rel.related_name:
- self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
+ self.rel.related_name = self.rel.related_name % {
+ 'class': cls.__name__.lower(),
+ 'app_label': cls._meta.app_label.lower(),
+ }
other = self.rel.to
- if isinstance(other, basestring):
+ if isinstance(other, basestring) or other._meta.pk is None:
def resolve_related_class(field, model, cls):
field.rel.to = model
field.do_related_class(model, cls)
@@ -116,31 +121,28 @@
if not cls._meta.abstract:
self.contribute_to_related_class(other, self.related)
- def get_db_prep_lookup(self, lookup_type, value):
- # If we are doing a lookup on a Related Field, we must be
- # comparing object instances. The value should be the PK of value,
- # not value itself.
- def pk_trace(value):
- # Value may be a primary key, or an object held in a relation.
- # If it is an object, then we need to get the primary key value for
- # that object. In certain conditions (especially one-to-one relations),
- # the primary key may itself be an object - so we need to keep drilling
- # down until we hit a value that can be used for a comparison.
- v, field = value, None
- try:
- while True:
- v, field = getattr(v, v._meta.pk.name), v._meta.pk
- except AttributeError:
- pass
+ def get_prep_lookup(self, lookup_type, value):
+ if hasattr(value, 'prepare'):
+ return value.prepare()
+ if hasattr(value, '_prepare'):
+ return value._prepare()
+ # FIXME: lt and gt are explicitly allowed to make
+ # get_(next/prev)_by_date work; other lookups are not allowed since that
+ # gets messy pretty quick. This is a good candidate for some refactoring
+ # in the future.
+ if lookup_type in ['exact', 'gt', 'lt', 'gte', 'lte']:
+ return self._pk_trace(value, 'get_prep_lookup', lookup_type)
+ if lookup_type in ('range', 'in'):
+ return [self._pk_trace(v, 'get_prep_lookup', lookup_type) for v in value]
+ elif lookup_type == 'isnull':
+ return []
+ raise TypeError("Related Field has invalid lookup: %s" % lookup_type)
- if field:
- if lookup_type in ('range', 'in'):
- v = [v]
- v = field.get_db_prep_lookup(lookup_type, v)
- if isinstance(v, list):
- v = v[0]
- return v
-
+ def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
+ if not prepared:
+ value = self.get_prep_lookup(lookup_type, value)
+ if hasattr(value, 'get_compiler'):
+ value = value.get_compiler(connection=connection)
if hasattr(value, 'as_sql') or hasattr(value, '_as_sql'):
# If the value has a relabel_aliases method, it will need to
# be invoked before the final SQL is evaluated
@@ -149,27 +151,59 @@
if hasattr(value, 'as_sql'):
sql, params = value.as_sql()
else:
- sql, params = value._as_sql()
+ sql, params = value._as_sql(connection=connection)
return QueryWrapper(('(%s)' % sql), params)
- # FIXME: lt and gt are explicitally allowed to make
+ # FIXME: lt and gt are explicitly allowed to make
# get_(next/prev)_by_date work; other lookups are not allowed since that
# gets messy pretty quick. This is a good candidate for some refactoring
# in the future.
if lookup_type in ['exact', 'gt', 'lt', 'gte', 'lte']:
- return [pk_trace(value)]
+ return [self._pk_trace(value, 'get_db_prep_lookup', lookup_type,
+ connection=connection, prepared=prepared)]
if lookup_type in ('range', 'in'):
- return [pk_trace(v) for v in value]
+ return [self._pk_trace(v, 'get_db_prep_lookup', lookup_type,
+ connection=connection, prepared=prepared)
+ for v in value]
elif lookup_type == 'isnull':
return []
- raise TypeError, "Related Field has invalid lookup: %s" % lookup_type
+ raise TypeError("Related Field has invalid lookup: %s" % lookup_type)
- def _get_related_query_name(self, opts):
+ def _pk_trace(self, value, prep_func, lookup_type, **kwargs):
+ # Value may be a primary key, or an object held in a relation.
+ # If it is an object, then we need to get the primary key value for
+ # that object. In certain conditions (especially one-to-one relations),
+ # the primary key may itself be an object - so we need to keep drilling
+ # down until we hit a value that can be used for a comparison.
+ v = value
+ try:
+ while True:
+ v = getattr(v, v._meta.pk.name)
+ except AttributeError:
+ pass
+ except exceptions.ObjectDoesNotExist:
+ v = None
+
+ field = self
+ while field.rel:
+ if hasattr(field.rel, 'field_name'):
+ field = field.rel.to._meta.get_field(field.rel.field_name)
+ else:
+ field = field.rel.to._meta.pk
+
+ if lookup_type in ('range', 'in'):
+ v = [v]
+ v = getattr(field, prep_func)(lookup_type, v, **kwargs)
+ if isinstance(v, list):
+ v = v[0]
+ return v
+
+ def related_query_name(self):
# This method defines the name that can be used to identify this
# related object in a table-spanning query. It uses the lower-cased
# object_name by default, but this can be overridden with the
# "related_name" option.
- return self.rel.related_name or opts.object_name.lower()
+ return self.rel.related_name or self.opts.object_name.lower()
class SingleRelatedObjectDescriptor(object):
# This class provides the functionality that makes the related-object
@@ -179,7 +213,7 @@
# SingleRelatedObjectDescriptor instance.
def __init__(self, related):
self.related = related
- self.cache_name = '_%s_cache' % related.get_accessor_name()
+ self.cache_name = related.get_cache_name()
def __get__(self, instance, instance_type=None):
if instance is None:
@@ -188,13 +222,14 @@
return getattr(instance, self.cache_name)
except AttributeError:
params = {'%s__pk' % self.related.field.name: instance._get_pk_val()}
- rel_obj = self.related.model._base_manager.get(**params)
+ db = router.db_for_read(self.related.model, instance=instance)
+ rel_obj = self.related.model._base_manager.using(db).get(**params)
setattr(instance, self.cache_name, rel_obj)
return rel_obj
def __set__(self, instance, value):
if instance is None:
- raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name
+ raise AttributeError("%s must be accessed via instance" % self.related.opts.object_name)
# The similarity of the code below to the code in
# ReverseSingleRelatedObjectDescriptor is annoying, but there's a bunch
@@ -209,6 +244,15 @@
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
(value, instance._meta.object_name,
self.related.get_accessor_name(), self.related.opts.object_name))
+ elif value is not None:
+ if instance._state.db is None:
+ instance._state.db = router.db_for_write(instance.__class__, instance=value)
+ elif value._state.db is None:
+ value._state.db = router.db_for_write(value.__class__, instance=instance)
+ elif value._state.db is not None and instance._state.db is not None:
+ if not router.allow_relation(value, instance):
+ raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' %
+ (value, instance._state.db, value._state.db))
# Set the value of the related field to the value of the related object's related field
setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname))
@@ -251,16 +295,17 @@
# If the related manager indicates that it should be used for
# related fields, respect that.
rel_mgr = self.field.rel.to._default_manager
+ db = router.db_for_read(self.field.rel.to, instance=instance)
if getattr(rel_mgr, 'use_for_related_fields', False):
- rel_obj = rel_mgr.get(**params)
+ rel_obj = rel_mgr.using(db).get(**params)
else:
- rel_obj = QuerySet(self.field.rel.to).get(**params)
+ rel_obj = QuerySet(self.field.rel.to).using(db).get(**params)
setattr(instance, cache_name, rel_obj)
return rel_obj
def __set__(self, instance, value):
if instance is None:
- raise AttributeError, "%s must be accessed via instance" % self._field.name
+ raise AttributeError("%s must be accessed via instance" % self._field.name)
# If null=True, we can assign null here, but otherwise the value needs
# to be an instance of the related class.
@@ -271,6 +316,15 @@
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
(value, instance._meta.object_name,
self.field.name, self.field.rel.to._meta.object_name))
+ elif value is not None:
+ if instance._state.db is None:
+ instance._state.db = router.db_for_write(instance.__class__, instance=value)
+ elif value._state.db is None:
+ value._state.db = router.db_for_write(value.__class__, instance=instance)
+ elif value._state.db is not None and instance._state.db is not None:
+ if not router.allow_relation(value, instance):
+ raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' %
+ (value, instance._state.db, value._state.db))
# If we're setting the value of a OneToOneField to None, we need to clear
# out the cache on any old related object. Otherwise, deleting the
@@ -289,7 +343,7 @@
# cache. This cache also might not exist if the related object
# hasn't been accessed yet.
if related:
- cache_name = '_%s_cache' % self.field.related.get_accessor_name()
+ cache_name = self.field.related.get_cache_name()
try:
delattr(related, cache_name)
except AttributeError:
@@ -325,7 +379,7 @@
def __set__(self, instance, value):
if instance is None:
- raise AttributeError, "Manager must be accessed via instance"
+ raise AttributeError("Manager must be accessed via instance")
manager = self.__get__(instance)
# If the foreign key can support nulls, then completely clear the related set.
@@ -352,26 +406,29 @@
class RelatedManager(superclass):
def get_query_set(self):
- return superclass.get_query_set(self).filter(**(self.core_filters))
+ db = self._db or router.db_for_read(rel_model, instance=instance)
+ return superclass.get_query_set(self).using(db).filter(**(self.core_filters))
def add(self, *objs):
for obj in objs:
if not isinstance(obj, self.model):
- raise TypeError, "'%s' instance expected" % self.model._meta.object_name
+ raise TypeError("'%s' instance expected" % self.model._meta.object_name)
setattr(obj, rel_field.name, instance)
obj.save()
add.alters_data = True
def create(self, **kwargs):
kwargs.update({rel_field.name: instance})
- return super(RelatedManager, self).create(**kwargs)
+ db = router.db_for_write(rel_model, instance=instance)
+ return super(RelatedManager, self).using(db).create(**kwargs)
create.alters_data = True
def get_or_create(self, **kwargs):
# Update kwargs with the related object that this
# ForeignRelatedObjectsDescriptor knows about.
kwargs.update({rel_field.name: instance})
- return super(RelatedManager, self).get_or_create(**kwargs)
+ db = router.db_for_write(rel_model, instance=instance)
+ return super(RelatedManager, self).using(db).get_or_create(**kwargs)
get_or_create.alters_data = True
# remove() and clear() are only provided if the ForeignKey can have a value of null.
@@ -384,7 +441,7 @@
setattr(obj, rel_field.name, None)
obj.save()
else:
- raise rel_field.rel.to.DoesNotExist, "%r is not related to %r." % (obj, instance)
+ raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, instance))
remove.alters_data = True
def clear(self):
@@ -401,68 +458,74 @@
return manager
-def create_many_related_manager(superclass, through=False):
+def create_many_related_manager(superclass, rel=False):
"""Creates a manager that subclasses 'superclass' (which is a Manager)
and adds behavior for many-to-many related objects."""
+ through = rel.through
class ManyRelatedManager(superclass):
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
- join_table=None, source_col_name=None, target_col_name=None):
+ join_table=None, source_field_name=None, target_field_name=None,
+ reverse=False):
super(ManyRelatedManager, self).__init__()
self.core_filters = core_filters
self.model = model
self.symmetrical = symmetrical
self.instance = instance
- self.join_table = join_table
- self.source_col_name = source_col_name
- self.target_col_name = target_col_name
+ self.source_field_name = source_field_name
+ self.target_field_name = target_field_name
self.through = through
- self._pk_val = self.instance._get_pk_val()
+ self._pk_val = self.instance.pk
+ self.reverse = reverse
if self._pk_val is None:
raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
def get_query_set(self):
- return superclass.get_query_set(self)._next_is_sticky().filter(**(self.core_filters))
+ db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance)
+ return superclass.get_query_set(self).using(db)._next_is_sticky().filter(**(self.core_filters))
# If the ManyToMany relation has an intermediary model,
# the add and remove methods do not exist.
- if through is None:
+ if rel.through._meta.auto_created:
def add(self, *objs):
- self._add_items(self.source_col_name, self.target_col_name, *objs)
+ self._add_items(self.source_field_name, self.target_field_name, *objs)
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
if self.symmetrical:
- self._add_items(self.target_col_name, self.source_col_name, *objs)
+ self._add_items(self.target_field_name, self.source_field_name, *objs)
add.alters_data = True
def remove(self, *objs):
- self._remove_items(self.source_col_name, self.target_col_name, *objs)
+ self._remove_items(self.source_field_name, self.target_field_name, *objs)
# If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
if self.symmetrical:
- self._remove_items(self.target_col_name, self.source_col_name, *objs)
+ self._remove_items(self.target_field_name, self.source_field_name, *objs)
remove.alters_data = True
def clear(self):
- self._clear_items(self.source_col_name)
+ self._clear_items(self.source_field_name)
# If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
if self.symmetrical:
- self._clear_items(self.target_col_name)
+ self._clear_items(self.target_field_name)
clear.alters_data = True
def create(self, **kwargs):
# This check needs to be done here, since we can't later remove this
# from the method lookup table, as we do with add and remove.
- if through is not None:
- raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
- new_obj = super(ManyRelatedManager, self).create(**kwargs)
+ if not rel.through._meta.auto_created:
+ opts = through._meta
+ raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
+ db = router.db_for_write(self.instance.__class__, instance=self.instance)
+ new_obj = super(ManyRelatedManager, self).using(db).create(**kwargs)
self.add(new_obj)
return new_obj
create.alters_data = True
def get_or_create(self, **kwargs):
+ db = router.db_for_write(self.instance.__class__, instance=self.instance)
obj, created = \
- super(ManyRelatedManager, self).get_or_create(**kwargs)
+ super(ManyRelatedManager, self).using(db).get_or_create(**kwargs)
# We only need to add() if created because if we got an object back
# from get() then the relationship already exists.
if created:
@@ -470,41 +533,54 @@
return obj, created
get_or_create.alters_data = True
- def _add_items(self, source_col_name, target_col_name, *objs):
+ def _add_items(self, source_field_name, target_field_name, *objs):
# join_table: name of the m2m link table
- # source_col_name: the PK colname in join_table for the source object
- # target_col_name: the PK colname in join_table for the target object
+ # source_field_name: the PK fieldname in join_table for the source object
+ # target_field_name: the PK fieldname in join_table for the target object
# *objs - objects to add. Either object instances, or primary keys of object instances.
# If there aren't any objects, there is nothing to do.
+ from django.db.models import Model
if objs:
- from django.db.models.base import Model
- # Check that all the objects are of the right type
new_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- new_ids.add(obj._get_pk_val())
+ if not router.allow_relation(obj, self.instance):
+ raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
+ (obj, self.instance._state.db, obj._state.db))
+ new_ids.add(obj.pk)
elif isinstance(obj, Model):
- raise TypeError, "'%s' instance expected" % self.model._meta.object_name
+ raise TypeError("'%s' instance expected" % self.model._meta.object_name)
else:
new_ids.add(obj)
- # Add the newly created or already existing objects to the join table.
- # First find out which items are already added, to avoid adding them twice
- cursor = connection.cursor()
- cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \
- (target_col_name, self.join_table, source_col_name,
- target_col_name, ",".join(['%s'] * len(new_ids))),
- [self._pk_val] + list(new_ids))
- existing_ids = set([row[0] for row in cursor.fetchall()])
+ db = router.db_for_write(self.through.__class__, instance=self.instance)
+ vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
+ vals = vals.filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: new_ids,
+ })
+ new_ids = new_ids - set(vals)
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action='pre_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids)
# Add the ones that aren't there already
- for obj_id in (new_ids - existing_ids):
- cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
- (self.join_table, source_col_name, target_col_name),
- [self._pk_val, obj_id])
- transaction.commit_unless_managed()
+ for obj_id in new_ids:
+ self.through._default_manager.using(db).create(**{
+ '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % target_field_name: obj_id,
+ })
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are inserting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action='post_add',
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=new_ids)
- def _remove_items(self, source_col_name, target_col_name, *objs):
+ def _remove_items(self, source_field_name, target_field_name, *objs):
# source_col_name: the PK colname in join_table for the source object
# target_col_name: the PK colname in join_table for the target object
# *objs - objects to remove
@@ -515,24 +591,46 @@
old_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- old_ids.add(obj._get_pk_val())
+ old_ids.add(obj.pk)
else:
old_ids.add(obj)
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are deleting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action="pre_remove",
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=old_ids)
# Remove the specified objects from the join table
- cursor = connection.cursor()
- cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \
- (self.join_table, source_col_name,
- target_col_name, ",".join(['%s'] * len(old_ids))),
- [self._pk_val] + list(old_ids))
- transaction.commit_unless_managed()
+ db = router.db_for_write(self.through.__class__, instance=self.instance)
+ self.through._default_manager.using(db).filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: old_ids
+ }).delete()
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are deleting the
+ # duplicate data row for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action="post_remove",
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=old_ids)
- def _clear_items(self, source_col_name):
+ def _clear_items(self, source_field_name):
# source_col_name: the PK colname in join_table for the source object
- cursor = connection.cursor()
- cursor.execute("DELETE FROM %s WHERE %s = %%s" % \
- (self.join_table, source_col_name),
- [self._pk_val])
- transaction.commit_unless_managed()
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are clearing the
+ # duplicate data rows for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action="pre_clear",
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=None)
+ db = router.db_for_write(self.through.__class__, instance=self.instance)
+ self.through._default_manager.using(db).filter(**{
+ source_field_name: self._pk_val
+ }).delete()
+ if self.reverse or source_field_name == self.source_field_name:
+ # Don't send the signal when we are clearing the
+ # duplicate data rows for symmetrical reverse entries.
+ signals.m2m_changed.send(sender=rel.through, action="post_clear",
+ instance=self.instance, reverse=self.reverse,
+ model=self.model, pk_set=None)
return ManyRelatedManager
@@ -554,33 +652,33 @@
# model's default manager.
rel_model = self.related.model
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
+ RelatedManager = create_many_related_manager(superclass, self.related.field.rel)
- qn = connection.ops.quote_name
manager = RelatedManager(
model=rel_model,
core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
instance=instance,
symmetrical=False,
- join_table=qn(self.related.field.m2m_db_table()),
- source_col_name=qn(self.related.field.m2m_reverse_name()),
- target_col_name=qn(self.related.field.m2m_column_name())
+ source_field_name=self.related.field.m2m_reverse_field_name(),
+ target_field_name=self.related.field.m2m_field_name(),
+ reverse=True
)
return manager
def __set__(self, instance, value):
if instance is None:
- raise AttributeError, "Manager must be accessed via instance"
+ raise AttributeError("Manager must be accessed via instance")
- through = getattr(self.related.field.rel, 'through', None)
- if through is not None:
- raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+ if not self.related.field.rel.through._meta.auto_created:
+ opts = self.related.field.rel.through._meta
+ raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
manager = self.__get__(instance)
manager.clear()
manager.add(*value)
+
class ReverseManyRelatedObjectsDescriptor(object):
# This class provides the functionality that makes the related-object
# managers available as attributes on a model class, for fields that have
@@ -591,6 +689,13 @@
def __init__(self, m2m_field):
self.field = m2m_field
+ def _through(self):
+ # through is provided so that you have easy access to the through
+ # model (Book.authors.through) for inlines, etc. This is done as
+ # a property to ensure that the fully resolved value is returned.
+ return self.field.rel.through
+ through = property(_through)
+
def __get__(self, instance, instance_type=None):
if instance is None:
return self
@@ -599,28 +704,27 @@
# model's default manager.
rel_model=self.field.rel.to
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
+ RelatedManager = create_many_related_manager(superclass, self.field.rel)
- qn = connection.ops.quote_name
manager = RelatedManager(
model=rel_model,
core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
instance=instance,
- symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
- join_table=qn(self.field.m2m_db_table()),
- source_col_name=qn(self.field.m2m_column_name()),
- target_col_name=qn(self.field.m2m_reverse_name())
+ symmetrical=self.field.rel.symmetrical,
+ source_field_name=self.field.m2m_field_name(),
+ target_field_name=self.field.m2m_reverse_field_name(),
+ reverse=False
)
return manager
def __set__(self, instance, value):
if instance is None:
- raise AttributeError, "Manager must be accessed via instance"
+ raise AttributeError("Manager must be accessed via instance")
- through = getattr(self.field.rel, 'through', None)
- if through is not None:
- raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+ if not self.field.rel.through._meta.auto_created:
+ opts = self.field.rel.through._meta
+ raise AttributeError("Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name))
manager = self.__get__(instance)
manager.clear()
@@ -642,6 +746,10 @@
self.multiple = True
self.parent_link = parent_link
+ def is_hidden(self):
+ "Should the related object be hidden?"
+ return self.related_name and self.related_name[-1] == '+'
+
def get_related_field(self):
"""
Returns the Field in the 'to' object to which this relationship is
@@ -673,6 +781,10 @@
self.multiple = True
self.through = through
+ def is_hidden(self):
+ "Should the related object be hidden?"
+ return self.related_name and self.related_name[-1] == '+'
+
def get_related_field(self):
"""
Returns the field in the to' object to which this relationship is tied
@@ -683,6 +795,10 @@
class ForeignKey(RelatedField, Field):
empty_strings_allowed = False
+ default_error_messages = {
+ 'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
+ }
+ description = _("Foreign Key (type determined by related field)")
def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try:
to_name = to._meta.object_name.lower()
@@ -690,7 +806,10 @@
assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
else:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
- to_field = to_field or to._meta.pk.name
+ # For backwards compatibility purposes, we need to *try* and set
+ # the to_field during FK construction. It won't be guaranteed to
+ # be correct until contribute_to_class is called. Refs #12190.
+ to_field = to_field or (to._meta.pk and to._meta.pk.name)
kwargs['verbose_name'] = kwargs.get('verbose_name', None)
kwargs['rel'] = rel_class(to, to_field,
@@ -702,6 +821,19 @@
self.db_index = True
+ def validate(self, value, model_instance):
+ if self.rel.parent_link:
+ return
+ super(ForeignKey, self).validate(value, model_instance)
+ if value is None:
+ return
+
+ qs = self.rel.to._default_manager.filter(**{self.rel.field_name:value})
+ qs = qs.complex_filter(self.rel.limit_choices_to)
+ if not qs.exists():
+ raise exceptions.ValidationError(self.error_messages['invalid'] % {
+ 'model': self.rel.to._meta.verbose_name, 'pk': value})
+
def get_attname(self):
return '%s_id' % self.name
@@ -715,11 +847,12 @@
return getattr(field_default, self.rel.get_related_field().attname)
return field_default
- def get_db_prep_save(self, value):
+ def get_db_prep_save(self, value, connection):
if value == '' or value == None:
return None
else:
- return self.rel.get_related_field().get_db_prep_save(value)
+ return self.rel.get_related_field().get_db_prep_save(value,
+ connection=connection)
def value_to_string(self, obj):
if not obj:
@@ -743,19 +876,24 @@
cls._meta.duplicate_targets[self.column] = (target, "o2m")
def contribute_to_related_class(self, cls, related):
- setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
+ # Internal FK's - i.e., those with a related name ending with '+' -
+ # don't get a related descriptor.
+ if not self.rel.is_hidden():
+ setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
+ if self.rel.field_name is None:
+ self.rel.field_name = cls._meta.pk.name
def formfield(self, **kwargs):
+ db = kwargs.pop('using', None)
defaults = {
'form_class': forms.ModelChoiceField,
- 'queryset': self.rel.to._default_manager.complex_filter(
- self.rel.limit_choices_to),
+ 'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to),
'to_field_name': self.rel.field_name,
}
defaults.update(kwargs)
return super(ForeignKey, self).formfield(**defaults)
- def db_type(self):
+ def db_type(self, connection):
# The database column type of a ForeignKey is the column type
# of the field to which it points. An exception is if the ForeignKey
# points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField,
@@ -767,8 +905,8 @@
(not connection.features.related_fields_match_type and
isinstance(rel_field, (PositiveIntegerField,
PositiveSmallIntegerField)))):
- return IntegerField().db_type()
- return rel_field.db_type()
+ return IntegerField().db_type(connection=connection)
+ return rel_field.db_type(connection=connection)
class OneToOneField(ForeignKey):
"""
@@ -777,6 +915,7 @@
always returns the object pointed to (since there will only ever be one),
rather than returning a list.
"""
+ description = _("One-to-one relationship")
def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@@ -790,7 +929,55 @@
return None
return super(OneToOneField, self).formfield(**kwargs)
+ def save_form_data(self, instance, data):
+ if isinstance(data, self.rel.to):
+ setattr(instance, self.name, data)
+ else:
+ setattr(instance, self.attname, data)
+
+def create_many_to_many_intermediary_model(field, klass):
+ from django.db import models
+ managed = True
+ if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
+ to_model = field.rel.to
+ to = to_model.split('.')[-1]
+ def set_managed(field, model, cls):
+ field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
+ add_lazy_relation(klass, field, to_model, set_managed)
+ elif isinstance(field.rel.to, basestring):
+ to = klass._meta.object_name
+ to_model = klass
+ managed = klass._meta.managed
+ else:
+ to = field.rel.to._meta.object_name
+ to_model = field.rel.to
+ managed = klass._meta.managed or to_model._meta.managed
+ name = '%s_%s' % (klass._meta.object_name, field.name)
+ if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or to == klass._meta.object_name:
+ from_ = 'from_%s' % to.lower()
+ to = 'to_%s' % to.lower()
+ else:
+ from_ = klass._meta.object_name.lower()
+ to = to.lower()
+ meta = type('Meta', (object,), {
+ 'db_table': field._get_m2m_db_table(klass._meta),
+ 'managed': managed,
+ 'auto_created': klass,
+ 'app_label': klass._meta.app_label,
+ 'unique_together': (from_, to),
+ 'verbose_name': '%(from)s-%(to)s relationship' % {'from': from_, 'to': to},
+ 'verbose_name_plural': '%(from)s-%(to)s relationships' % {'from': from_, 'to': to},
+ })
+ # Construct and return the new class.
+ return type(name, (models.Model,), {
+ 'Meta': meta,
+ '__module__': klass.__module__,
+ from_: models.ForeignKey(klass, related_name='%s+' % name),
+ to: models.ForeignKey(to_model, related_name='%s+' % name)
+ })
+
class ManyToManyField(RelatedField, Field):
+ description = _("Many-to-many relationship")
def __init__(self, to, **kwargs):
try:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
@@ -801,19 +988,16 @@
kwargs['rel'] = ManyToManyRel(to,
related_name=kwargs.pop('related_name', None),
limit_choices_to=kwargs.pop('limit_choices_to', None),
- symmetrical=kwargs.pop('symmetrical', True),
+ symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT),
through=kwargs.pop('through', None))
self.db_table = kwargs.pop('db_table', None)
if kwargs['rel'].through is not None:
- self.creates_table = False
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
- else:
- self.creates_table = True
Field.__init__(self, **kwargs)
- msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.')
+ msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
self.help_text = string_concat(self.help_text, ' ', msg)
def get_choices_default(self):
@@ -822,62 +1006,45 @@
def _get_m2m_db_table(self, opts):
"Function that can be curried to provide the m2m table name for this relation"
if self.rel.through is not None:
- return self.rel.through_model._meta.db_table
+ return self.rel.through._meta.db_table
elif self.db_table:
return self.db_table
else:
return util.truncate_name('%s_%s' % (opts.db_table, self.name),
connection.ops.max_name_length())
- def _get_m2m_column_name(self, related):
- "Function that can be curried to provide the source column name for the m2m table"
- try:
- return self._m2m_column_name_cache
- except:
- if self.rel.through is not None:
- for f in self.rel.through_model._meta.fields:
- if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
- self._m2m_column_name_cache = f.column
- break
- # If this is an m2m relation to self, avoid the inevitable name clash
- elif related.model == related.parent_model:
- self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id'
- else:
- self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id'
-
- # Return the newly cached value
- return self._m2m_column_name_cache
+ def _get_m2m_attr(self, related, attr):
+ "Function that can be curried to provide the source accessor or DB column name for the m2m table"
+ cache_attr = '_m2m_%s_cache' % attr
+ if hasattr(self, cache_attr):
+ return getattr(self, cache_attr)
+ for f in self.rel.through._meta.fields:
+ if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
+ setattr(self, cache_attr, getattr(f, attr))
+ return getattr(self, cache_attr)
- def _get_m2m_reverse_name(self, related):
- "Function that can be curried to provide the related column name for the m2m table"
- try:
- return self._m2m_reverse_name_cache
- except:
- if self.rel.through is not None:
- found = False
- for f in self.rel.through_model._meta.fields:
- if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
- if related.model == related.parent_model:
- # If this is an m2m-intermediate to self,
- # the first foreign key you find will be
- # the source column. Keep searching for
- # the second foreign key.
- if found:
- self._m2m_reverse_name_cache = f.column
- break
- else:
- found = True
- else:
- self._m2m_reverse_name_cache = f.column
- break
- # If this is an m2m relation to self, avoid the inevitable name clash
- elif related.model == related.parent_model:
- self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id'
- else:
- self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id'
-
- # Return the newly cached value
- return self._m2m_reverse_name_cache
+ def _get_m2m_reverse_attr(self, related, attr):
+ "Function that can be curried to provide the related accessor or DB column name for the m2m table"
+ cache_attr = '_m2m_reverse_%s_cache' % attr
+ if hasattr(self, cache_attr):
+ return getattr(self, cache_attr)
+ found = False
+ for f in self.rel.through._meta.fields:
+ if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
+ if related.model == related.parent_model:
+ # If this is an m2m-intermediate to self,
+ # the first foreign key you find will be
+ # the source column. Keep searching for
+ # the second foreign key.
+ if found:
+ setattr(self, cache_attr, getattr(f, attr))
+ break
+ else:
+ found = True
+ else:
+ setattr(self, cache_attr, getattr(f, attr))
+ break
+ return getattr(self, cache_attr)
def isValidIDList(self, field_data, all_data):
"Validates that the value is a valid list of foreign keys"
@@ -919,10 +1086,17 @@
# specify *what* on my non-reversible relation?!"), so we set it up
# automatically. The funky name reduces the chance of an accidental
# clash.
- if self.rel.symmetrical and self.rel.to == "self" and self.rel.related_name is None:
+ if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
self.rel.related_name = "%s_rel_+" % name
super(ManyToManyField, self).contribute_to_class(cls, name)
+
+ # The intermediate m2m model is not auto created if:
+ # 1) There is a manually specified intermediate, or
+ # 2) The class owning the m2m field is abstract.
+ if not self.rel.through and not cls._meta.abstract:
+ self.rel.through = create_many_to_many_intermediary_model(self, cls)
+
# Add the descriptor for the m2m relation
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
@@ -933,11 +1107,8 @@
# work correctly.
if isinstance(self.rel.through, basestring):
def resolve_through_model(field, model, cls):
- field.rel.through_model = model
+ field.rel.through = model
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
- elif self.rel.through:
- self.rel.through_model = self.rel.through
- self.rel.through = self.rel.through._meta.object_name
if isinstance(self.rel.to, basestring):
target = self.rel.to
@@ -946,15 +1117,17 @@
cls._meta.duplicate_targets[self.column] = (target, "m2m")
def contribute_to_related_class(self, cls, related):
- # m2m relations to self do not have a ManyRelatedObjectsDescriptor,
- # as it would be redundant - unless the field is non-symmetrical.
- if related.model != related.parent_model or not self.rel.symmetrical:
- # Add the descriptor for the m2m relation
+ # Internal M2Ms (i.e., those with a related name ending with '+')
+ # don't get a related descriptor.
+ if not self.rel.is_hidden():
setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
# Set up the accessors for the column names on the m2m table
- self.m2m_column_name = curry(self._get_m2m_column_name, related)
- self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related)
+ self.m2m_column_name = curry(self._get_m2m_attr, related, 'column')
+ self.m2m_reverse_name = curry(self._get_m2m_reverse_attr, related, 'column')
+
+ self.m2m_field_name = curry(self._get_m2m_attr, related, 'name')
+ self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name')
def set_attributes_from_rel(self):
pass
@@ -967,7 +1140,11 @@
setattr(instance, self.attname, data)
def formfield(self, **kwargs):
- defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)}
+ db = kwargs.pop('using', None)
+ defaults = {
+ 'form_class': forms.ModelMultipleChoiceField,
+ 'queryset': self.rel.to._default_manager.using(db).complex_filter(self.rel.limit_choices_to)
+ }
defaults.update(kwargs)
# If initial is passed in, it's a list of related objects, but the
# MultipleChoiceField takes a list of IDs.
@@ -978,7 +1155,7 @@
defaults['initial'] = [i._get_pk_val() for i in initial]
return super(ManyToManyField, self).formfield(**defaults)
- def db_type(self):
+ def db_type(self, connection):
# A ManyToManyField is not represented by a single column,
# so return None.
return None