diff options
Diffstat (limited to 'django/db/models/fields/related.py')
| -rw-r--r-- | django/db/models/fields/related.py | 173 |
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: |
