summaryrefslogtreecommitdiff
path: root/django/db/models/fields/related.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/db/models/fields/related.py')
-rw-r--r--django/db/models/fields/related.py173
1 files changed, 125 insertions, 48 deletions
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 735dda4969..ab2b9a6c5e 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -2,11 +2,10 @@ from django.db import connection, transaction
from django.db.models import signals, get_model
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.text import capfirst
from django.utils.translation import ugettext_lazy, string_concat, ungettext, ugettext as _
from django.utils.functional import curry
-from django.utils.encoding import smart_unicode
from django.core import validators
from django import oldforms
from django import forms
@@ -24,7 +23,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
pending_lookups = {}
-def add_lazy_relation(cls, field, relation):
+def add_lazy_relation(cls, field, relation, operation):
"""
Adds a lookup on ``cls`` when a related field is defined using a string,
i.e.::
@@ -46,6 +45,8 @@ def add_lazy_relation(cls, field, relation):
If the other model hasn't yet been loaded -- almost a given if you're using
lazy relationships -- then the relation won't be set up until the
class_prepared signal fires at the end of model initialization.
+
+ operation is the work that must be performed once the relation can be resolved.
"""
# Check for recursive relations
if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
@@ -67,11 +68,10 @@ def add_lazy_relation(cls, field, relation):
# is prepared.
model = get_model(app_label, model_name, False)
if model:
- field.rel.to = model
- field.do_related_class(model, cls)
+ operation(field, model, cls)
else:
key = (app_label, model_name)
- value = (cls, field)
+ value = (cls, field, operation)
pending_lookups.setdefault(key, []).append(value)
def do_pending_lookups(sender):
@@ -79,9 +79,8 @@ def do_pending_lookups(sender):
Handle any pending relations to the sending model. Sent from class_prepared.
"""
key = (sender._meta.app_label, sender.__name__)
- for cls, field in pending_lookups.pop(key, []):
- field.rel.to = sender
- field.do_related_class(sender, cls)
+ for cls, field, operation in pending_lookups.pop(key, []):
+ operation(field, sender, cls)
dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
@@ -109,13 +108,17 @@ class RelatedField(object):
other = self.rel.to
if isinstance(other, basestring):
- add_lazy_relation(cls, self, other)
+ def resolve_related_class(field, model, cls):
+ field.rel.to = model
+ field.do_related_class(model, cls)
+ add_lazy_relation(cls, self, other, resolve_related_class)
else:
self.do_related_class(other, cls)
def set_attributes_from_rel(self):
self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name)
- self.verbose_name = self.verbose_name or self.rel.to._meta.verbose_name
+ if self.verbose_name is None:
+ self.verbose_name = self.rel.to._meta.verbose_name
self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name
def do_related_class(self, other, cls):
@@ -236,7 +239,14 @@ class ReverseSingleRelatedObjectDescriptor(object):
params = {'%s__pk' % self.field.rel.field_name: val}
else:
params = {'%s__exact' % self.field.rel.field_name: val}
- rel_obj = self.field.rel.to._default_manager.get(**params)
+
+ # If the related manager indicates that it should be used for
+ # related fields, respect that.
+ rel_mgr = self.field.rel.to._default_manager
+ if getattr(rel_mgr, 'use_for_related_fields', False):
+ rel_obj = rel_mgr.get(**params)
+ else:
+ rel_obj = QuerySet(self.field.rel.to).get(**params)
setattr(instance, cache_name, rel_obj)
return rel_obj
@@ -340,7 +350,7 @@ class ForeignRelatedObjectsDescriptor(object):
manager.clear()
manager.add(*value)
-def create_many_related_manager(superclass):
+def create_many_related_manager(superclass, through=False):
"""Creates a manager that subclasses 'superclass' (which is a Manager)
and adds behavior for many-to-many related objects."""
class ManyRelatedManager(superclass):
@@ -354,6 +364,7 @@ def create_many_related_manager(superclass):
self.join_table = join_table
self.source_col_name = source_col_name
self.target_col_name = target_col_name
+ self.through = through
self._pk_val = self.instance._get_pk_val()
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__)
@@ -361,21 +372,24 @@ def create_many_related_manager(superclass):
def get_query_set(self):
return superclass.get_query_set(self).filter(**(self.core_filters))
- def add(self, *objs):
- self._add_items(self.source_col_name, self.target_col_name, *objs)
+ # If the ManyToMany relation has an intermediary model,
+ # the add and remove methods do not exist.
+ if through is None:
+ def add(self, *objs):
+ self._add_items(self.source_col_name, self.target_col_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)
- add.alters_data = True
+ # 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)
+ add.alters_data = True
- def remove(self, *objs):
- self._remove_items(self.source_col_name, self.target_col_name, *objs)
+ def remove(self, *objs):
+ self._remove_items(self.source_col_name, self.target_col_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)
- remove.alters_data = True
+ # 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)
+ remove.alters_data = True
def clear(self):
self._clear_items(self.source_col_name)
@@ -386,6 +400,10 @@ def create_many_related_manager(superclass):
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 = self.model(**kwargs)
new_obj.save()
self.add(new_obj)
@@ -473,7 +491,7 @@ class ManyRelatedObjectsDescriptor(object):
# model's default manager.
rel_model = self.related.model
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass)
+ RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
qn = connection.ops.quote_name
manager = RelatedManager(
@@ -492,6 +510,10 @@ class ManyRelatedObjectsDescriptor(object):
if instance is None:
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
+
manager = self.__get__(instance)
manager.clear()
manager.add(*value)
@@ -514,7 +536,7 @@ class ReverseManyRelatedObjectsDescriptor(object):
# model's default manager.
rel_model=self.field.rel.to
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass)
+ RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
qn = connection.ops.quote_name
manager = RelatedManager(
@@ -533,6 +555,10 @@ class ReverseManyRelatedObjectsDescriptor(object):
if instance is None:
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
+
manager = self.__get__(instance)
manager.clear()
manager.add(*value)
@@ -584,7 +610,7 @@ class OneToOneRel(ManyToOneRel):
class ManyToManyRel(object):
def __init__(self, to, num_in_admin=0, related_name=None,
- limit_choices_to=None, symmetrical=True):
+ limit_choices_to=None, symmetrical=True, through=None):
self.to = to
self.num_in_admin = num_in_admin
self.related_name = related_name
@@ -594,6 +620,7 @@ class ManyToManyRel(object):
self.edit_inline = False
self.symmetrical = symmetrical
self.multiple = True
+ self.through = through
class ForeignKey(RelatedField, Field):
empty_strings_allowed = False
@@ -604,12 +631,7 @@ class ForeignKey(RelatedField, Field):
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:
to_field = to_field or to._meta.pk.name
- kwargs['verbose_name'] = kwargs.get('verbose_name', '')
-
- if 'edit_inline_type' in kwargs:
- import warnings
- warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.", DeprecationWarning)
- kwargs['edit_inline'] = kwargs.pop('edit_inline_type')
+ kwargs['verbose_name'] = kwargs.get('verbose_name', None)
kwargs['rel'] = rel_class(to, to_field,
num_in_admin=kwargs.pop('num_in_admin', 3),
@@ -705,6 +727,7 @@ class OneToOneField(ForeignKey):
"""
def __init__(self, to, to_field=None, **kwargs):
kwargs['unique'] = True
+ kwargs['editable'] = False
if 'num_in_admin' not in kwargs:
kwargs['num_in_admin'] = 0
super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
@@ -722,8 +745,16 @@ class ManyToManyField(RelatedField, Field):
num_in_admin=kwargs.pop('num_in_admin', 0),
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', True),
+ 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.')
@@ -738,26 +769,62 @@ class ManyToManyField(RelatedField, Field):
def _get_m2m_db_table(self, opts):
"Function that can be curried to provide the m2m table name for this relation"
- if self.db_table:
+ if self.rel.through is not None:
+ return self.rel.through_model._meta.db_table
+ elif self.db_table:
return self.db_table
else:
return '%s_%s' % (opts.db_table, self.name)
def _get_m2m_column_name(self, related):
"Function that can be curried to provide the source column name for the m2m table"
- # If this is an m2m relation to self, avoid the inevitable name clash
- if related.model == related.parent_model:
- return 'from_' + related.model._meta.object_name.lower() + '_id'
- else:
- return related.model._meta.object_name.lower() + '_id'
+ 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_reverse_name(self, related):
"Function that can be curried to provide the related column name for the m2m table"
- # If this is an m2m relation to self, avoid the inevitable name clash
- if related.model == related.parent_model:
- return 'to_' + related.parent_model._meta.object_name.lower() + '_id'
- else:
- return related.parent_model._meta.object_name.lower() + '_id'
+ 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 isValidIDList(self, field_data, all_data):
"Validates that the value is a valid list of foreign keys"
@@ -791,13 +858,23 @@ class ManyToManyField(RelatedField, Field):
return new_data
def contribute_to_class(self, cls, name):
- super(ManyToManyField, self).contribute_to_class(cls, name)
+ super(ManyToManyField, self).contribute_to_class(cls, name)
# Add the descriptor for the m2m relation
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
# Set up the accessor for the m2m table name for the relation
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
-
+
+ # Populate some necessary rel arguments so that cross-app relations
+ # work correctly.
+ if isinstance(self.rel.through, basestring):
+ def resolve_through_model(field, model, cls):
+ field.rel.through_model = 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
else: