diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index be2580134e..a7c4fca2fd 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -459,13 +459,69 @@ class ReverseManyRelatedObjectsDescriptor(object): 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): + 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 + 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 + + def get_related_field(self): + """ + Returns the Field in the 'to' object to which this relationship is + tied. + """ + return self.to._meta.get_field_by_name(self.field_name, True)[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): + # 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, related_name, limit_choices_to, lookup_overrides, + raw_id_admin) + 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): + 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" + class ForeignKey(RelatedField, Field): empty_strings_allowed = False - def __init__(self, to, to_field=None, **kwargs): + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, basestring), "ForeignKey(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (to, 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: to_field = to_field or to._meta.pk.name kwargs['verbose_name'] = kwargs.get('verbose_name', '') @@ -475,7 +531,7 @@ class ForeignKey(RelatedField, Field): warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - kwargs['rel'] = ManyToOneRel(to, to_field, + 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), @@ -563,82 +619,25 @@ class ForeignKey(RelatedField, Field): return IntegerField().db_type() return rel_field.db_type() -class OneToOneField(RelatedField, IntegerField): +class OneToOneField(ForeignKey): + """ + A OneToOneField is essentially the same as a ForeignKey, with the exception + that always carries a "unique" constraint with it and the reverse relation + always returns the object pointed to (since there will only ever be one), + rather than returning a list. + """ def __init__(self, to, to_field=None, **kwargs): - try: - to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT - assert isinstance(to, basestring), "OneToOneField(%r) is invalid. First parameter to OneToOneField must be either a model, a model name, or the string %r" % (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.") - kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - - kwargs['rel'] = OneToOneRel(to, to_field, - num_in_admin=kwargs.pop('num_in_admin', 0), - 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)) - kwargs['primary_key'] = True - IntegerField.__init__(self, **kwargs) - - self.db_index = True - - def get_attname(self): - return '%s_id' % self.name - - def get_validator_unique_lookup_type(self): - return '%s__%s__exact' % (self.name, self.rel.get_related_field().name) - - # TODO: Copied from ForeignKey... putting this in RelatedField adversely affects - # ManyToManyField. This works for now. - 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 contribute_to_class(self, cls, name): - super(OneToOneField, self).contribute_to_class(cls, name) - setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self)) + 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): - setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) + setattr(cls, related.get_accessor_name(), + SingleRelatedObjectDescriptor(related)) if not cls._meta.one_to_one_field: cls._meta.one_to_one_field = self - def formfield(self, **kwargs): - defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.all()} - defaults.update(kwargs) - return super(OneToOneField, self).formfield(**defaults) - - def db_type(self): - # The database column type of a OneToOneField is the column type - # of the field to which it points. An exception is if the OneToOneField - # points to an AutoField/PositiveIntegerField/PositiveSmallIntegerField, - # in which case the column type is simply that of an IntegerField. - rel_field = self.rel.get_related_field() - if isinstance(rel_field, (AutoField, PositiveIntegerField, PositiveSmallIntegerField)): - return IntegerField().db_type() - return rel_field.db_type() - class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', None) @@ -770,59 +769,3 @@ class ManyToManyField(RelatedField, Field): # so return None. return None -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): - 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 - 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 - - def get_related_field(self): - """ - Returns the Field in the 'to' object to which this relationship is - tied. - """ - return self.to._meta.get_field_by_name(self.field_name, True)[0] - -class OneToOneRel(ManyToOneRel): - def __init__(self, to, field_name, num_in_admin=0, edit_inline=False, - related_name=None, limit_choices_to=None, lookup_overrides=None, - raw_id_admin=False): - self.to, self.field_name = to, field_name - self.num_in_admin, self.edit_inline = num_in_admin, edit_inline - 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 = 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): - 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" diff --git a/docs/model-api.txt b/docs/model-api.txt index 3f908ec158..6b8310176b 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -979,9 +979,6 @@ the relationship should work. All are optional: One-to-one relationships ~~~~~~~~~~~~~~~~~~~~~~~~ -The semantics of one-to-one relationships will be changing soon, so we don't -recommend you use them. If that doesn't scare you away, keep reading. - To define a one-to-one relationship, use ``OneToOneField``. You use it just like any other ``Field`` type: by including it as a class attribute of your model. @@ -1003,9 +1000,11 @@ As with ``ForeignKey``, a relationship to self can be defined by using the string ``"self"`` instead of the model name; references to as-yet undefined models can be made by using a string containing the model name. -This ``OneToOneField`` will actually replace the primary key ``id`` field -(since one-to-one relations share the same primary key), and will be displayed -as a read-only field when you edit an object in the admin interface: +**New in Django development version:** ``OneToOneField`` classes used to +automatically become the primary key on a model. This is no longer true, +although you can manually pass in the ``primary_key`` attribute if you like. +Thus, it's now possible to have multilpe fields of type ``OneToOneField`` on a +single model. See the `One-to-one relationship model example`_ for a full example. diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index 2f3f22e628..ab478c6118 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -16,7 +16,7 @@ class Place(models.Model): return u"%s the place" % self.name class Restaurant(models.Model): - place = models.OneToOneField(Place) + place = models.OneToOneField(Place, primary_key=True) serves_hot_dogs = models.BooleanField() serves_pizza = models.BooleanField() @@ -38,6 +38,14 @@ class RelatedModel(models.Model): link = models.OneToOneField(ManualPrimaryKey) name = models.CharField(max_length = 50) +class MultiModel(models.Model): + link1 = models.OneToOneField(Place) + link2 = models.OneToOneField(ManualPrimaryKey) + name = models.CharField(max_length=50) + + def __unicode__(self): + return u"Multimodel %s" % self.name + __test__ = {'API_TESTS':""" # Create a couple of Places. >>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') @@ -63,8 +71,8 @@ Traceback (most recent call last): ... DoesNotExist: Restaurant matching query does not exist. -# Set the place using assignment notation. Because place is the primary key on Restaurant, -# the save will create a new restaurant +# Set the place using assignment notation. Because place is the primary key on +# Restaurant, the save will create a new restaurant >>> r.place = p2 >>> r.save() >>> p2.restaurant @@ -72,9 +80,9 @@ DoesNotExist: Restaurant matching query does not exist. >>> r.place -# Set the place back again, using assignment in the reverse direction -# Need to reget restaurant object first, because the reverse set -# can't update the existing restaurant instance +# Set the place back again, using assignment in the reverse direction. +# Need to reget restaurant object first, because the reverse set can't update +# the existing restaurant instance >>> p1.restaurant = r >>> r.save() >>> p1.restaurant @@ -86,8 +94,7 @@ DoesNotExist: Restaurant matching query does not exist. # Restaurant.objects.all() just returns the Restaurants, not the Places. # Note that there are two restaurants - Ace Hardware the Restaurant was created -# in the call to r.place = p2. This means there are multiple restaurants referencing -# a single place... +# in the call to r.place = p2. >>> Restaurant.objects.all() [, ] @@ -165,4 +172,17 @@ DoesNotExist: Restaurant matching query does not exist. >>> o1.save() >>> o2 = RelatedModel(link=o1, name="secondary") >>> o2.save() + +# You can have multiple one-to-one fields on a model, too. +>>> x1 = MultiModel(link1=p1, link2=o1, name="x1") +>>> x1.save() +>>> o1.multimodel + + +# This will fail because each one-to-one field must be unique (and link2=o1 was +# used for x1, above). +>>> MultiModel(link1=p2, link2=o1, name="x1").save() +Traceback (most recent call last): + ... +IntegrityError: ... """} diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index 0ccc19f895..be1b2fcd28 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -22,7 +22,7 @@ class Author(models.Model): class Meta: ordering = ('name',) - + def __unicode__(self): return self.name @@ -39,21 +39,21 @@ class Article(models.Model): return self.headline class AuthorProfile(models.Model): - author = models.OneToOneField(Author) + author = models.OneToOneField(Author, primary_key=True) date_of_birth = models.DateField() - + def __unicode__(self): return u"Profile of %s" % self.author - + class Actor(models.Model): name = models.CharField(max_length=20, primary_key=True) class Meta: ordering = ('name',) - + def __unicode__(self): return self.name - + class Movie(models.Model): actor = models.ForeignKey(Actor) title = models.CharField(max_length=50) @@ -63,7 +63,7 @@ class Movie(models.Model): def __unicode__(self): return self.title - + class Score(models.Model): score = models.FloatField() @@ -100,7 +100,7 @@ __test__ = {'API_TESTS':""" >>> dom = minidom.parseString(xml) # Deserializing has a similar interface, except that special DeserializedObject -# instances are returned. This is because data might have changed in the +# instances are returned. This is because data might have changed in the # database since the data was serialized (we'll simulate that below). >>> for obj in serializers.deserialize("xml", xml): ... print obj @@ -148,7 +148,7 @@ __test__ = {'API_TESTS':""" >>> Article.objects.all() [, ] -# If you use your own primary key field (such as a OneToOneField), +# If you use your own primary key field (such as a OneToOneField), # it doesn't appear in the serialized field list - it replaces the # pk identifier. >>> profile = AuthorProfile(author=joe, date_of_birth=datetime(1970,1,1)) @@ -186,7 +186,7 @@ __test__ = {'API_TESTS':""" >>> print serializers.serialize("json", Article.objects.all(), fields=('headline','pub_date')) [{"pk": 1, "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 2, "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:11"}}, {"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}] -# Every string is serialized as a unicode object, also primary key +# Every string is serialized as a unicode object, also primary key # which is 'varchar' >>> ac = Actor(name="Zażółć") >>> mv = Movie(title="Gęślą jaźń", actor=ac) @@ -247,12 +247,12 @@ try: pk: 2 ->>> obs = list(serializers.deserialize("yaml", serialized)) ->>> for i in obs: +>>> obs = list(serializers.deserialize("yaml", serialized)) +>>> for i in obs: ... print i """ except ImportError: pass - + diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py index e9df508822..bd3a279d55 100644 --- a/tests/regressiontests/serializers_regress/models.py +++ b/tests/regressiontests/serializers_regress/models.py @@ -77,7 +77,7 @@ class USStateData(models.Model): class XMLData(models.Model): data = models.XMLField(null=True) - + class Tag(models.Model): """A tag on an item.""" data = models.SlugField() @@ -93,36 +93,36 @@ class GenericData(models.Model): data = models.CharField(max_length=30) tags = generic.GenericRelation(Tag) - + # The following test classes are all for validation # of related objects; in particular, forward, backward, # and self references. - + class Anchor(models.Model): - """This is a model that can be used as + """This is a model that can be used as something for other models to point at""" - + data = models.CharField(max_length=30) class UniqueAnchor(models.Model): - """This is a model that can be used as + """This is a model that can be used as something for other models to point at""" data = models.CharField(unique=True, max_length=30) - + class FKData(models.Model): data = models.ForeignKey(Anchor, null=True) - + class M2MData(models.Model): data = models.ManyToManyField(Anchor, null=True) - + class O2OData(models.Model): - # One to one field can't be null, since it is a PK. - data = models.OneToOneField(Anchor) + # One to one field can't be null here, since it is a PK. + data = models.OneToOneField(Anchor, primary_key=True) class FKSelfData(models.Model): data = models.ForeignKey('self', null=True) - + class M2MSelfData(models.Model): data = models.ManyToManyField('self', null=True, symmetrical=False) @@ -142,7 +142,7 @@ class FKDataToO2O(models.Model): class BooleanPKData(models.Model): data = models.BooleanField(primary_key=True) - + class CharPKData(models.Model): data = models.CharField(max_length=30, primary_key=True)