From a2de78b3b8db5784cc1aecefd90f833ba7d9532e Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
Date: Tue, 20 Aug 2013 01:52:32 -0400
Subject: [PATCH] Fixed #21216 -- Allow `OneToOneField` reverse accessor to be
hidden.
---
django/db/models/fields/related.py | 8 +++-----
docs/releases/1.7.txt | 5 +++++
tests/one_to_one_regress/models.py | 15 +++++++++++++--
tests/one_to_one_regress/tests.py | 16 +++++++++++++---
4 files changed, 34 insertions(+), 10 deletions(-)
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 6cd8d19..aa2a83b 100644
a
|
b
|
class ManyToManyRel(object):
|
960 | 960 | class ForeignObject(RelatedField): |
961 | 961 | requires_unique_target = True |
962 | 962 | generate_reverse_relation = True |
| 963 | related_accessor_class = ForeignRelatedObjectsDescriptor |
963 | 964 | |
964 | 965 | def __init__(self, to, from_fields, to_fields, **kwargs): |
965 | 966 | self.from_fields = from_fields |
… |
… |
class ForeignObject(RelatedField):
|
1160 | 1161 | # Internal FK's - i.e., those with a related name ending with '+' - |
1161 | 1162 | # and swapped models don't get a related descriptor. |
1162 | 1163 | if not self.rel.is_hidden() and not related.model._meta.swapped: |
1163 | | setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) |
| 1164 | setattr(cls, related.get_accessor_name(), self.related_accessor_class(related)) |
1164 | 1165 | if self.rel.limit_choices_to: |
1165 | 1166 | cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) |
1166 | 1167 | |
… |
… |
class OneToOneField(ForeignKey):
|
1334 | 1335 | always returns the object pointed to (since there will only ever be one), |
1335 | 1336 | rather than returning a list. |
1336 | 1337 | """ |
| 1338 | related_accessor_class = SingleRelatedObjectDescriptor |
1337 | 1339 | description = _("One-to-one relationship") |
1338 | 1340 | |
1339 | 1341 | def __init__(self, to, to_field=None, **kwargs): |
… |
… |
class OneToOneField(ForeignKey):
|
1346 | 1348 | del kwargs['unique'] |
1347 | 1349 | return name, path, args, kwargs |
1348 | 1350 | |
1349 | | def contribute_to_related_class(self, cls, related): |
1350 | | setattr(cls, related.get_accessor_name(), |
1351 | | SingleRelatedObjectDescriptor(related)) |
1352 | | |
1353 | 1351 | def formfield(self, **kwargs): |
1354 | 1352 | if self.rel.parent_link: |
1355 | 1353 | return None |
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index d613220..8f650a1 100644
a
|
b
|
Models
|
289 | 289 | * Explicit :class:`~django.db.models.OneToOneField` for |
290 | 290 | :ref:`multi-table-inheritance` are now discovered in abstract classes. |
291 | 291 | |
| 292 | * Is it now possible to avoid creating a backward relation for |
| 293 | :class:`~django.db.models.OneToOneField` by setting its |
| 294 | :attr:`~django.db.models.ForeignKey.related_name` to |
| 295 | `'+'` or ending it with `'+'`. |
| 296 | |
292 | 297 | Signals |
293 | 298 | ^^^^^^^ |
294 | 299 | |
diff --git a/tests/one_to_one_regress/models.py b/tests/one_to_one_regress/models.py
index 9b65edf..3cd4e17 100644
a
|
b
|
class Place(models.Model):
|
12 | 12 | def __str__(self): |
13 | 13 | return "%s the place" % self.name |
14 | 14 | |
| 15 | |
15 | 16 | @python_2_unicode_compatible |
16 | 17 | class Restaurant(models.Model): |
17 | 18 | place = models.OneToOneField(Place) |
… |
… |
class Restaurant(models.Model):
|
21 | 22 | def __str__(self): |
22 | 23 | return "%s the restaurant" % self.place.name |
23 | 24 | |
| 25 | |
24 | 26 | @python_2_unicode_compatible |
25 | 27 | class Bar(models.Model): |
26 | 28 | place = models.OneToOneField(Place) |
… |
… |
class Bar(models.Model):
|
29 | 31 | def __str__(self): |
30 | 32 | return "%s the bar" % self.place.name |
31 | 33 | |
| 34 | |
32 | 35 | class UndergroundBar(models.Model): |
33 | 36 | place = models.OneToOneField(Place, null=True) |
34 | 37 | serves_cocktails = models.BooleanField(default=True) |
35 | 38 | |
| 39 | |
36 | 40 | @python_2_unicode_compatible |
37 | 41 | class Favorites(models.Model): |
38 | | name = models.CharField(max_length = 50) |
| 42 | name = models.CharField(max_length=50) |
39 | 43 | restaurants = models.ManyToManyField(Restaurant) |
40 | 44 | |
41 | 45 | def __str__(self): |
42 | 46 | return "Favorites for %s" % self.name |
43 | 47 | |
| 48 | |
44 | 49 | class Target(models.Model): |
45 | 50 | pass |
46 | 51 | |
| 52 | |
47 | 53 | class Pointer(models.Model): |
48 | 54 | other = models.OneToOneField(Target, primary_key=True) |
49 | 55 | |
| 56 | |
50 | 57 | class Pointer2(models.Model): |
51 | | other = models.OneToOneField(Target) |
| 58 | other = models.OneToOneField(Target, related_name='second_pointer') |
| 59 | |
| 60 | |
| 61 | class HiddenPointer(models.Model): |
| 62 | target = models.OneToOneField(Target, related_name='hidden+') |
diff --git a/tests/one_to_one_regress/tests.py b/tests/one_to_one_regress/tests.py
index 3dfe3e4..da4309e 100644
a
|
b
|
from __future__ import unicode_literals
|
2 | 2 | |
3 | 3 | from django.test import TestCase |
4 | 4 | |
5 | | from .models import Place, Restaurant, Bar, Favorites, Target, UndergroundBar |
| 5 | from .models import (Bar, Favorites, HiddenPointer, Place, Restaurant, Target, |
| 6 | UndergroundBar) |
6 | 7 | |
7 | 8 | |
8 | 9 | class OneToOneRegressionTests(TestCase): |
… |
… |
class OneToOneRegressionTests(TestCase):
|
125 | 126 | [] |
126 | 127 | ) |
127 | 128 | self.assertQuerysetEqual( |
128 | | Target.objects.filter(pointer2=None), |
| 129 | Target.objects.filter(second_pointer=None), |
129 | 130 | ['<Target: Target object>'] |
130 | 131 | ) |
131 | 132 | self.assertQuerysetEqual( |
132 | | Target.objects.exclude(pointer2=None), |
| 133 | Target.objects.exclude(second_pointer=None), |
133 | 134 | [] |
134 | 135 | ) |
135 | 136 | |
… |
… |
class OneToOneRegressionTests(TestCase):
|
250 | 251 | self.p1.delete() |
251 | 252 | self.assertTrue(UndergroundBar.objects.filter(pk=u.pk).exists()) |
252 | 253 | self.assertIsNone(UndergroundBar.objects.get(pk=u.pk).place) |
| 254 | |
| 255 | def test_hidden_accessor(self): |
| 256 | """ |
| 257 | When a '+' ending related name is specified no reverse accessor should |
| 258 | be added to the related model. |
| 259 | """ |
| 260 | self.assertFalse( |
| 261 | hasattr(Target, HiddenPointer._meta.get_field('target').related.get_accessor_name()) |
| 262 | ) |