1
0
mirror of https://github.com/django/django.git synced 2025-07-06 10:49:17 +00:00

queryset-refactor: Fixed up OneToOneFields (mostly).

They now share as much code as possible with ForeignKeys, but behave more or
less as they did before (the backwards incompatible change is that they are no
longer automatically primary keys -- so more than one per model is permitted).

The documentation still uses an example that is better suited to model
inheritance, but that will change in due course. Also, the admin interface
still shows them as read-only fields, which is probably wrong now, but that can
change on newforms-admin after this branch is merged into trunk.


git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7096 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-02-08 09:49:17 +00:00
parent 55cd025670
commit accc20d799
5 changed files with 131 additions and 169 deletions

View File

@ -459,13 +459,69 @@ class ReverseManyRelatedObjectsDescriptor(object):
manager.clear() manager.clear()
manager.add(*value) 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): class ForeignKey(RelatedField, Field):
empty_strings_allowed = False empty_strings_allowed = False
def __init__(self, to, to_field=None, **kwargs): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs):
try: try:
to_name = to._meta.object_name.lower() to_name = to._meta.object_name.lower()
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT 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: else:
to_field = to_field or to._meta.pk.name to_field = to_field or to._meta.pk.name
kwargs['verbose_name'] = kwargs.get('verbose_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.") warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.")
kwargs['edit_inline'] = kwargs.pop('edit_inline_type') 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), num_in_admin=kwargs.pop('num_in_admin', 3),
min_num_in_admin=kwargs.pop('min_num_in_admin', None), min_num_in_admin=kwargs.pop('min_num_in_admin', None),
max_num_in_admin=kwargs.pop('max_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 IntegerField().db_type()
return rel_field.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): def __init__(self, to, to_field=None, **kwargs):
try: kwargs['unique'] = True
to_name = to._meta.object_name.lower() if 'num_in_admin' not in kwargs:
except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT kwargs['num_in_admin'] = 0
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) super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs)
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))
def contribute_to_related_class(self, cls, related): 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: if not cls._meta.one_to_one_field:
cls._meta.one_to_one_field = self 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): class ManyToManyField(RelatedField, Field):
def __init__(self, to, **kwargs): def __init__(self, to, **kwargs):
kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['verbose_name'] = kwargs.get('verbose_name', None)
@ -770,59 +769,3 @@ class ManyToManyField(RelatedField, Field):
# so return None. # so return None.
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"

View File

@ -979,9 +979,6 @@ the relationship should work. All are optional:
One-to-one relationships 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 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 like any other ``Field`` type: by including it as a class attribute of your
model. 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 string ``"self"`` instead of the model name; references to as-yet undefined
models can be made by using a string containing the model name. models can be made by using a string containing the model name.
This ``OneToOneField`` will actually replace the primary key ``id`` field **New in Django development version:** ``OneToOneField`` classes used to
(since one-to-one relations share the same primary key), and will be displayed automatically become the primary key on a model. This is no longer true,
as a read-only field when you edit an object in the admin interface: 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. See the `One-to-one relationship model example`_ for a full example.

View File

@ -16,7 +16,7 @@ class Place(models.Model):
return u"%s the place" % self.name return u"%s the place" % self.name
class Restaurant(models.Model): class Restaurant(models.Model):
place = models.OneToOneField(Place) place = models.OneToOneField(Place, primary_key=True)
serves_hot_dogs = models.BooleanField() serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField() serves_pizza = models.BooleanField()
@ -38,6 +38,14 @@ class RelatedModel(models.Model):
link = models.OneToOneField(ManualPrimaryKey) link = models.OneToOneField(ManualPrimaryKey)
name = models.CharField(max_length = 50) 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':""" __test__ = {'API_TESTS':"""
# Create a couple of Places. # Create a couple of Places.
>>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') >>> 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. DoesNotExist: Restaurant matching query does not exist.
# Set the place using assignment notation. Because place is the primary key on Restaurant, # Set the place using assignment notation. Because place is the primary key on
# the save will create a new restaurant # Restaurant, the save will create a new restaurant
>>> r.place = p2 >>> r.place = p2
>>> r.save() >>> r.save()
>>> p2.restaurant >>> p2.restaurant
@ -72,9 +80,9 @@ DoesNotExist: Restaurant matching query does not exist.
>>> r.place >>> r.place
<Place: Ace Hardware the place> <Place: Ace Hardware the place>
# Set the place back again, using assignment in the reverse direction # Set the place back again, using assignment in the reverse direction.
# Need to reget restaurant object first, because the reverse set # Need to reget restaurant object first, because the reverse set can't update
# can't update the existing restaurant instance # the existing restaurant instance
>>> p1.restaurant = r >>> p1.restaurant = r
>>> r.save() >>> r.save()
>>> p1.restaurant >>> p1.restaurant
@ -86,8 +94,7 @@ DoesNotExist: Restaurant matching query does not exist.
# Restaurant.objects.all() just returns the Restaurants, not the Places. # Restaurant.objects.all() just returns the Restaurants, not the Places.
# Note that there are two restaurants - Ace Hardware the Restaurant was created # 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 # in the call to r.place = p2.
# a single place...
>>> Restaurant.objects.all() >>> Restaurant.objects.all()
[<Restaurant: Demon Dogs the restaurant>, <Restaurant: Ace Hardware the restaurant>] [<Restaurant: Demon Dogs the restaurant>, <Restaurant: Ace Hardware the restaurant>]
@ -165,4 +172,17 @@ DoesNotExist: Restaurant matching query does not exist.
>>> o1.save() >>> o1.save()
>>> o2 = RelatedModel(link=o1, name="secondary") >>> o2 = RelatedModel(link=o1, name="secondary")
>>> o2.save() >>> 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
<MultiModel: Multimodel x1>
# 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: ...
"""} """}

View File

@ -39,7 +39,7 @@ class Article(models.Model):
return self.headline return self.headline
class AuthorProfile(models.Model): class AuthorProfile(models.Model):
author = models.OneToOneField(Author) author = models.OneToOneField(Author, primary_key=True)
date_of_birth = models.DateField() date_of_birth = models.DateField()
def __unicode__(self): def __unicode__(self):

View File

@ -117,8 +117,8 @@ class M2MData(models.Model):
data = models.ManyToManyField(Anchor, null=True) data = models.ManyToManyField(Anchor, null=True)
class O2OData(models.Model): class O2OData(models.Model):
# One to one field can't be null, since it is a PK. # One to one field can't be null here, since it is a PK.
data = models.OneToOneField(Anchor) data = models.OneToOneField(Anchor, primary_key=True)
class FKSelfData(models.Model): class FKSelfData(models.Model):
data = models.ForeignKey('self', null=True) data = models.ForeignKey('self', null=True)