diff -r 6641e941ef1e -r ff1a9aa48cfd app/django/db/models/fields/related.py --- a/app/django/db/models/fields/related.py Tue Oct 14 12:36:55 2008 +0000 +++ b/app/django/db/models/fields/related.py Tue Oct 14 16:00:59 2008 +0000 @@ -1,30 +1,25 @@ 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, get_ul_class, 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.text import capfirst +from django.utils.encoding import smart_unicode 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 newforms as forms -from django.dispatch import dispatcher +from django.core import exceptions +from django import forms try: set except NameError: from sets import Set as set # Python 2.3 fallback -# Values for Relation.edit_inline. -TABULAR, STACKED = 1, 2 - 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 +41,8 @@ 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,31 +64,21 @@ # 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): +def do_pending_lookups(sender, **kwargs): """ 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) - -dispatcher.connect(do_pending_lookups, signal=signals.class_prepared) + for cls, field, operation in pending_lookups.pop(key, []): + operation(field, sender, cls) -def manipulator_valid_rel_key(f, self, field_data, all_data): - "Validates that the value is a valid foreign key" - klass = f.rel.to - try: - klass._default_manager.get(**{f.rel.field_name: field_data}) - except klass.DoesNotExist: - raise validators.ValidationError, _("Please enter a valid %s.") % f.verbose_name +signals.class_prepared.connect(do_pending_lookups) #HACK class RelatedField(object): @@ -103,23 +90,30 @@ if hasattr(sup, 'contribute_to_class'): sup.contribute_to_class(cls, name) - other = self.rel.to - if isinstance(other, basestring): - add_lazy_relation(cls, self, other) - else: - self.do_related_class(other, cls) + if not cls._meta.abstract and self.rel.related_name: self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()} + other = self.rel.to + if isinstance(other, basestring): + 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): self.set_attributes_from_rel() related = RelatedObject(other, cls, self) - self.contribute_to_related_class(other, related) + if not cls._meta.abstract: + self.contribute_to_related_class(other, related) def get_db_prep_lookup(self, lookup_type, value): # If we are doing a lookup on a Related Field, we must be @@ -131,20 +125,31 @@ # 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 + v, field = value, None try: while True: - v = getattr(v, v._meta.pk.name) + v, field = getattr(v, v._meta.pk.name), v._meta.pk except AttributeError: pass + 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 if hasattr(value, 'as_sql'): sql, params = value.as_sql() return QueryWrapper(('(%s)' % sql), params) - if lookup_type == 'exact': + + # FIXME: lt and gt are explicitally 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']: return [pk_trace(value)] - if lookup_type == 'in': + if lookup_type in ('range', 'in'): return [pk_trace(v) for v in value] elif lookup_type == 'isnull': return [] @@ -182,14 +187,29 @@ def __set__(self, instance, value): if instance is None: 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 + # of small differences that would make a common base class convoluted. + + # If null=True, we can assign null here, but otherwise the value needs + # to be an instance of the related class. + if value is None and self.related.field.null == False: + raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % + (instance._meta.object_name, self.related.get_accessor_name())) + elif value is not None and not isinstance(value, self.related.model): + 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)) + # Set the value of the related field setattr(value, self.related.field.rel.get_related_field().attname, instance) - # Clear the cache, if it exists - try: - delattr(value, self.related.field.get_cache_name()) - except AttributeError: - pass + # Since we already know what the related object is, seed the related + # object caches now, too. This avoids another db hit if you get the + # object you just set. + setattr(instance, self.cache_name, value) + setattr(value, self.related.field.get_cache_name(), instance) class ReverseSingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object @@ -218,13 +238,31 @@ 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 def __set__(self, instance, value): if instance is None: 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. + if value is None and self.field.null == False: + raise ValueError('Cannot assign None: "%s.%s" does not allow null values.' % + (instance._meta.object_name, self.field.name)) + elif value is not None and not isinstance(value, self.field.rel.to): + 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)) + # Set the value of the related field try: val = getattr(value, self.field.rel.get_related_field().attname) @@ -232,11 +270,10 @@ val = None setattr(instance, self.field.attname, val) - # Clear the cache, if it exists - try: - delattr(instance, self.field.get_cache_name()) - except AttributeError: - pass + # Since we already know what the related object is, seed the related + # object cache now, too. This avoids another db hit if you get the + # object you just set. + setattr(instance, self.field.get_cache_name(), value) class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object @@ -269,11 +306,17 @@ add.alters_data = True def create(self, **kwargs): - new_obj = self.model(**kwargs) - self.add(new_obj) - return new_obj + kwargs.update({rel_field.name: instance}) + return super(RelatedManager, self).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) + get_or_create.alters_data = True + # remove() and clear() are only provided if the ForeignKey can have a value of null. if rel_field.null: def remove(self, *objs): @@ -294,7 +337,9 @@ clear.alters_data = True manager = RelatedManager() - manager.core_filters = {'%s__pk' % rel_field.name: getattr(instance, rel_field.rel.get_related_field().attname)} + attname = rel_field.rel.get_related_field().name + manager.core_filters = {'%s__%s' % (rel_field.name, attname): + getattr(instance, attname)} manager.model = self.related.model return manager @@ -310,7 +355,7 @@ 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): @@ -324,28 +369,32 @@ 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." % model) + 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).filter(**(self.core_filters)) + return superclass.get_query_set(self)._next_is_sticky().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) @@ -356,12 +405,25 @@ clear.alters_data = True def create(self, **kwargs): - new_obj = self.model(**kwargs) - new_obj.save() + # 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) self.add(new_obj) return new_obj create.alters_data = True + def get_or_create(self, **kwargs): + obj, created = \ + super(ManyRelatedManager, self).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: + self.add(obj) + return obj, created + get_or_create.alters_data = True + def _add_items(self, source_col_name, target_col_name, *objs): # join_table: name of the m2m link table # source_col_name: the PK colname in join_table for the source object @@ -443,7 +505,7 @@ # 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( @@ -462,6 +524,10 @@ 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) @@ -484,7 +550,7 @@ # 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( @@ -503,28 +569,27 @@ 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) class ManyToOneRel(object): - def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None, - max_num_in_admin=None, num_extra_on_change=1, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False, parent_link=False): + def __init__(self, to, field_name, related_name=None, + limit_choices_to=None, lookup_overrides=None, parent_link=False): try: to._meta except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT self.to, self.field_name = to, field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin - self.num_extra_on_change, self.related_name = num_extra_on_change, related_name + self.related_name = related_name if limit_choices_to is None: limit_choices_to = {} self.limit_choices_to = limit_choices_to self.lookup_overrides = lookup_overrides or {} - self.raw_id_admin = raw_id_admin self.multiple = True self.parent_link = parent_link @@ -540,36 +605,24 @@ return data[0] class OneToOneRel(ManyToOneRel): - def __init__(self, to, field_name, num_in_admin=0, min_num_in_admin=None, - max_num_in_admin=None, num_extra_on_change=None, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False, parent_link=False): - # NOTE: *_num_in_admin and num_extra_on_change are intentionally - # ignored here. We accept them as parameters only to match the calling - # signature of ManyToOneRel.__init__(). - super(OneToOneRel, self).__init__(to, field_name, num_in_admin, - edit_inline=edit_inline, related_name=related_name, - limit_choices_to=limit_choices_to, - lookup_overrides=lookup_overrides, raw_id_admin=raw_id_admin, - parent_link=parent_link) + def __init__(self, to, field_name, related_name=None, + limit_choices_to=None, lookup_overrides=None, parent_link=False): + super(OneToOneRel, self).__init__(to, field_name, + related_name=related_name, limit_choices_to=limit_choices_to, + lookup_overrides=lookup_overrides, parent_link=parent_link) self.multiple = False class ManyToManyRel(object): - def __init__(self, to, num_in_admin=0, related_name=None, - filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): + def __init__(self, to, related_name=None, limit_choices_to=None, + symmetrical=True, through=None): self.to = to - self.num_in_admin = num_in_admin self.related_name = related_name - self.filter_interface = filter_interface if limit_choices_to is None: limit_choices_to = {} self.limit_choices_to = limit_choices_to - self.edit_inline = False - self.raw_id_admin = raw_id_admin self.symmetrical = symmetrical self.multiple = True - - assert not (self.raw_id_admin and self.filter_interface), "ManyToManyRels may not use both raw_id_admin and filter_interface" + self.through = through class ForeignKey(RelatedField, Field): empty_strings_allowed = False @@ -579,24 +632,14 @@ except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 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 - 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), - min_num_in_admin=kwargs.pop('min_num_in_admin', None), - max_num_in_admin=kwargs.pop('max_num_in_admin', None), - num_extra_on_change=kwargs.pop('num_extra_on_change', 1), - edit_inline=kwargs.pop('edit_inline', False), related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), lookup_overrides=kwargs.pop('lookup_overrides', None), - raw_id_admin=kwargs.pop('raw_id_admin', False), parent_link=kwargs.pop('parent_link', False)) Field.__init__(self, **kwargs) @@ -608,23 +651,6 @@ def get_validator_unique_lookup_type(self): return '%s__%s__exact' % (self.name, self.rel.get_related_field().name) - def prepare_field_objs_and_params(self, manipulator, name_prefix): - params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname} - if self.rel.raw_id_admin: - field_objs = self.get_manipulator_field_objs() - params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) - else: - if self.radio_admin: - field_objs = [oldforms.RadioSelectField] - params['ul_class'] = get_ul_class(self.radio_admin) - else: - if self.null: - field_objs = [oldforms.NullSelectField] - else: - field_objs = [oldforms.SelectField] - params['choices'] = self.get_choices_default() - return field_objs, params - def get_default(self): "Here we check if the default value is an object and return the to_field if so." field_default = super(ForeignKey, self).get_default() @@ -632,44 +658,43 @@ return getattr(field_default, self.rel.get_related_field().attname) return field_default - def get_manipulator_field_objs(self): - rel_field = self.rel.get_related_field() - if self.rel.raw_id_admin and not isinstance(rel_field, AutoField): - return rel_field.get_manipulator_field_objs() - else: - return [oldforms.IntegerField] - def get_db_prep_save(self, value): if value == '' or value == None: return None else: return self.rel.get_related_field().get_db_prep_save(value) - def flatten_data(self, follow, obj=None): + def value_to_string(self, obj): if not obj: # In required many-to-one fields with only one available choice, # select that one available choice. Note: For SelectFields - # (radio_admin=False), we have to check that the length of choices - # is *2*, not 1, because SelectFields always have an initial - # "blank" value. Otherwise (radio_admin=True), we check that the - # length is 1. - if not self.blank and (not self.rel.raw_id_admin or self.choices): + # we have to check that the length of choices is *2*, not 1, + # because SelectFields always have an initial "blank" value. + if not self.blank and self.choices: choice_list = self.get_choices_default() - if self.radio_admin and len(choice_list) == 1: - return {self.attname: choice_list[0][0]} - if not self.radio_admin and len(choice_list) == 2: - return {self.attname: choice_list[1][0]} - return Field.flatten_data(self, follow, obj) + if len(choice_list) == 2: + return smart_unicode(choice_list[1][0]) + return Field.value_to_string(self, obj) def contribute_to_class(self, cls, name): super(ForeignKey, self).contribute_to_class(cls, name) setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + if isinstance(self.rel.to, basestring): + target = self.rel.to + else: + target = self.rel.to._meta.db_table + cls._meta.duplicate_targets[self.column] = (target, "o2m") def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) def formfield(self, **kwargs): - defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.all()} + defaults = { + 'form_class': forms.ModelChoiceField, + 'queryset': self.rel.to._default_manager.complex_filter( + self.rel.limit_choices_to), + 'to_field_name': self.rel.field_name, + } defaults.update(kwargs) return super(ForeignKey, self).formfield(**defaults) @@ -678,8 +703,13 @@ # of the field to which it points. An exception is if the ForeignKey # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, # in which case the column type is simply that of an IntegerField. + # If the database needs similar types for key fields however, the only + # thing we can do is making AutoField an IntegerField. rel_field = self.rel.get_related_field() - if isinstance(rel_field, (AutoField, PositiveIntegerField, PositiveSmallIntegerField)): + if (isinstance(rel_field, AutoField) or + (not connection.features.related_fields_match_type and + isinstance(rel_field, (PositiveIntegerField, + PositiveSmallIntegerField)))): return IntegerField().db_type() return rel_field.db_type() @@ -692,8 +722,6 @@ """ def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True - if 'num_in_admin' not in kwargs: - kwargs['num_in_admin'] = 0 super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) def contribute_to_related_class(self, cls, related): @@ -702,59 +730,98 @@ if not cls._meta.one_to_one_field: cls._meta.one_to_one_field = self + def formfield(self, **kwargs): + if self.rel.parent_link: + return None + return super(OneToOneField, self).formfield(**kwargs) + class ManyToManyField(RelatedField, Field): 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) + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) + kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = ManyToManyRel(to, - num_in_admin=kwargs.pop('num_in_admin', 0), related_name=kwargs.pop('related_name', None), - filter_interface=kwargs.pop('filter_interface', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - raw_id_admin=kwargs.pop('raw_id_admin', False), - 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"].raw_id_admin: - kwargs.setdefault("validator_list", []).append(self.isValidIDList) + 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) - if self.rel.raw_id_admin: - msg = ugettext_lazy('Separate multiple IDs with commas.') - else: - msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') + msg = ugettext_lazy('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) - def get_manipulator_field_objs(self): - if self.rel.raw_id_admin: - return [oldforms.RawIdAdminField] - else: - choices = self.get_choices_default() - return [curry(oldforms.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] - def get_choices_default(self): return Field.get_choices(self, include_blank=False) 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" @@ -767,30 +834,38 @@ objects = mod._default_manager.in_bulk(pks) if len(objects) != len(pks): badkeys = [k for k in pks if k not in objects] - raise validators.ValidationError, ungettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.", - "Please enter valid %(self)s IDs. The values %(value)r are invalid.", len(badkeys)) % { + raise exceptions.ValidationError( + ungettext("Please enter valid %(self)s IDs. The value %(value)r is invalid.", + "Please enter valid %(self)s IDs. The values %(value)r are invalid.", + len(badkeys)) % { 'self': self.verbose_name, 'value': len(badkeys) == 1 and badkeys[0] or tuple(badkeys), - } + }) - def flatten_data(self, follow, obj = None): - new_data = {} + def value_to_string(self, obj): + data = '' if obj: - instance_ids = [instance._get_pk_val() for instance in getattr(obj, self.name).all()] - if self.rel.raw_id_admin: - new_data[self.name] = u",".join([smart_unicode(id) for id in instance_ids]) - else: - new_data[self.name] = instance_ids + qs = getattr(obj, self.name).all() + data = [instance._get_pk_val() for instance in qs] else: # In required many-to-many fields with only one available choice, # select that one available choice. - if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin: + if not self.blank: choices_list = self.get_choices_default() if len(choices_list) == 1: - new_data[self.name] = [choices_list[0][0]] - return new_data + data = [choices_list[0][0]] + return smart_unicode(data) def contribute_to_class(self, cls, name): + # To support multiple relations to self, it's useful to have a non-None + # related name on symmetrical relations for internal reasons. The + # concept doesn't make a lot of sense externally ("you want me to + # 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: + self.rel.related_name = "%s_rel_+" % name + super(ManyToManyField, self).contribute_to_class(cls, name) # Add the descriptor for the m2m relation setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self)) @@ -798,6 +873,22 @@ # 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: + target = self.rel.to._meta.db_table + 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. @@ -820,7 +911,7 @@ setattr(instance, self.attname, data) def formfield(self, **kwargs): - defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.all()} + defaults = {'form_class': forms.ModelMultipleChoiceField, 'queryset': self.rel.to._default_manager.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.