From 1fd6e13bf2db025dc93482184439cc1a900db276 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Tue, 23 Sep 2014 23:29:17 +0700 Subject: [PATCH] Consolidated some many_to_one tests. --- tests/many_to_one/models.py | 68 +++++++++ tests/many_to_one/tests.py | 168 +++++++++++++++++++++-- tests/many_to_one_null/models.py | 8 ++ tests/many_to_one_null/tests.py | 9 +- tests/many_to_one_regress/__init__.py | 0 tests/many_to_one_regress/models.py | 69 ---------- tests/many_to_one_regress/tests.py | 137 ------------------ tests/reverse_single_related/__init__.py | 0 tests/reverse_single_related/models.py | 15 -- tests/reverse_single_related/tests.py | 46 ------- 10 files changed, 244 insertions(+), 276 deletions(-) delete mode 100644 tests/many_to_one_regress/__init__.py delete mode 100644 tests/many_to_one_regress/models.py delete mode 100644 tests/many_to_one_regress/tests.py delete mode 100644 tests/reverse_single_related/__init__.py delete mode 100644 tests/reverse_single_related/models.py delete mode 100644 tests/reverse_single_related/tests.py diff --git a/tests/many_to_one/models.py b/tests/many_to_one/models.py index 8c50ffe53bb..0b45ff0ef3a 100644 --- a/tests/many_to_one/models.py +++ b/tests/many_to_one/models.py @@ -30,3 +30,71 @@ class Article(models.Model): class Meta: ordering = ('headline',) + + +# If ticket #1578 ever slips back in, these models will not be able to be +# created (the field names being lower-cased versions of their opposite +# classes is important here). +class First(models.Model): + second = models.IntegerField() + + +class Second(models.Model): + first = models.ForeignKey(First, related_name='the_first') + + +# Protect against repetition of #1839, #2415 and #2536. +class Third(models.Model): + name = models.CharField(max_length=20) + third = models.ForeignKey('self', null=True, related_name='child_set') + + +class Parent(models.Model): + name = models.CharField(max_length=20, unique=True) + bestchild = models.ForeignKey('Child', null=True, related_name='favored_by') + + +class Child(models.Model): + name = models.CharField(max_length=20) + parent = models.ForeignKey(Parent) + + +class ToFieldChild(models.Model): + parent = models.ForeignKey(Parent, to_field='name') + + +# Multiple paths to the same model (#7110, #7125) +@python_2_unicode_compatible +class Category(models.Model): + name = models.CharField(max_length=20) + + def __str__(self): + return self.name + + +class Record(models.Model): + category = models.ForeignKey(Category) + + +@python_2_unicode_compatible +class Relation(models.Model): + left = models.ForeignKey(Record, related_name='left_set') + right = models.ForeignKey(Record, related_name='right_set') + + def __str__(self): + return "%s - %s" % (self.left.category.name, self.right.category.name) + + +# Test related objects visibility. +class SchoolManager(models.Manager): + def get_queryset(self): + return super(SchoolManager, self).get_queryset().filter(is_public=True) + + +class School(models.Model): + is_public = models.BooleanField(default=False) + objects = SchoolManager() + + +class Student(models.Model): + school = models.ForeignKey(School) diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index c02c247b296..f5949bd22e2 100644 --- a/tests/many_to_one/tests.py +++ b/tests/many_to_one/tests.py @@ -2,12 +2,13 @@ from copy import deepcopy import datetime from django.core.exceptions import MultipleObjectsReturned, FieldError -from django.db import transaction +from django.db import models, transaction from django.test import TestCase from django.utils import six from django.utils.translation import ugettext_lazy -from .models import Article, Reporter +from .models import (Article, Reporter, First, Third, Parent, Child, + ToFieldChild, Category, Record, Relation, School, Student) class ManyToOneTests(TestCase): @@ -374,12 +375,6 @@ class ManyToOneTests(TestCase): self.assertQuerysetEqual(Reporter.objects.all(), []) self.assertQuerysetEqual(Article.objects.all(), []) - def test_regression_12876(self): - # Regression for #12876 -- Model methods that include queries that - # recursive don't cause recursion depth problems under deepcopy. - self.r.cached_query = Article.objects.filter(reporter=self.r) - self.assertEqual(repr(deepcopy(self.r)), "") - def test_explicit_fk(self): # Create a new Article with get_or_create using an explicit value # for a ForeignKey. @@ -411,6 +406,12 @@ class ManyToOneTests(TestCase): repr(Article.objects.get(reporter_id=self.r2.id, pub_date=datetime.date(2011, 5, 7)))) + def test_deepcopy_and_circular_references(self): + # Regression for #12876 -- Model methods that include queries that + # recursive don't cause recursion depth problems under deepcopy. + self.r.cached_query = Article.objects.filter(reporter=self.r) + self.assertEqual(repr(deepcopy(self.r)), "") + def test_manager_class_caching(self): r1 = Reporter.objects.create(first_name='Mike') r2 = Reporter.objects.create(first_name='John') @@ -443,3 +444,154 @@ class ManyToOneTests(TestCase): expected_message % ', '.join(['EXTRA'] + Article._meta.get_all_field_names()), Article.objects.extra(select={'EXTRA': 'EXTRA_SELECT'}).values_list, 'notafield') + + def test_fk_assignment_and_related_object_cache(self): + # Tests of ForeignKey assignment and the related-object cache (see #6886). + + p = Parent.objects.create(name="Parent") + c = Child.objects.create(name="Child", parent=p) + + # Look up the object again so that we get a "fresh" object. + c = Child.objects.get(name="Child") + p = c.parent + + # Accessing the related object again returns the exactly same object. + self.assertTrue(c.parent is p) + + # But if we kill the cache, we get a new object. + del c._parent_cache + self.assertFalse(c.parent is p) + + # Assigning a new object results in that object getting cached immediately. + p2 = Parent.objects.create(name="Parent 2") + c.parent = p2 + self.assertTrue(c.parent is p2) + + # Assigning None succeeds if field is null=True. + p.bestchild = None + self.assertTrue(p.bestchild is None) + + # bestchild should still be None after saving. + p.save() + self.assertTrue(p.bestchild is None) + + # bestchild should still be None after fetching the object again. + p = Parent.objects.get(name="Parent") + self.assertTrue(p.bestchild is None) + + # Assigning None fails: Child.parent is null=False. + self.assertRaises(ValueError, setattr, c, "parent", None) + + # You also can't assign an object of the wrong type here + self.assertRaises(ValueError, setattr, c, "parent", First(id=1, second=1)) + + # Nor can you explicitly assign None to Child.parent during object + # creation (regression for #9649). + self.assertRaises(ValueError, Child, name='xyzzy', parent=None) + self.assertRaises(ValueError, Child.objects.create, name='xyzzy', parent=None) + + # Creation using keyword argument should cache the related object. + p = Parent.objects.get(name="Parent") + c = Child(parent=p) + self.assertTrue(c.parent is p) + + # Creation using keyword argument and unsaved related instance (#8070). + p = Parent() + with self.assertRaisesMessage(ValueError, + 'Cannot assign "%r": "%s" instance isn\'t saved in the database.' + % (p, Child.parent.field.rel.to._meta.object_name)): + Child(parent=p) + + with self.assertRaisesMessage(ValueError, + 'Cannot assign "%r": "%s" instance isn\'t saved in the database.' + % (p, Child.parent.field.rel.to._meta.object_name)): + ToFieldChild(parent=p) + + # Creation using attname keyword argument and an id will cause the + # related object to be fetched. + p = Parent.objects.get(name="Parent") + c = Child(parent_id=p.id) + self.assertFalse(c.parent is p) + self.assertEqual(c.parent, p) + + def test_multiple_foreignkeys(self): + # Test of multiple ForeignKeys to the same model (bug #7125). + c1 = Category.objects.create(name='First') + c2 = Category.objects.create(name='Second') + c3 = Category.objects.create(name='Third') + r1 = Record.objects.create(category=c1) + r2 = Record.objects.create(category=c1) + r3 = Record.objects.create(category=c2) + r4 = Record.objects.create(category=c2) + r5 = Record.objects.create(category=c3) + Relation.objects.create(left=r1, right=r2) + Relation.objects.create(left=r3, right=r4) + Relation.objects.create(left=r1, right=r3) + Relation.objects.create(left=r5, right=r2) + Relation.objects.create(left=r3, right=r2) + + q1 = Relation.objects.filter(left__category__name__in=['First'], right__category__name__in=['Second']) + self.assertQuerysetEqual(q1, [""]) + + q2 = Category.objects.filter(record__left_set__right__category__name='Second').order_by('name') + self.assertQuerysetEqual(q2, ["", ""]) + + p = Parent.objects.create(name="Parent") + c = Child.objects.create(name="Child", parent=p) + self.assertRaises(ValueError, Child.objects.create, name="Grandchild", parent=c) + + def test_fk_instantiation_outside_model(self): + # Regression for #12190 -- Should be able to instantiate a FK outside + # of a model, and interrogate its related field. + cat = models.ForeignKey(Category) + self.assertEqual('id', cat.rel.get_related_field().name) + + def test_relation_unsaved(self): + # Test that the _set manager does not join on Null value fields (#17541) + Third.objects.create(name='Third 1') + Third.objects.create(name='Third 2') + th = Third(name="testing") + # The object isn't saved an thus the relation field is null - we won't even + # execute a query in this case. + with self.assertNumQueries(0): + self.assertEqual(th.child_set.count(), 0) + th.save() + # Now the model is saved, so we will need to execute an query. + with self.assertNumQueries(1): + self.assertEqual(th.child_set.count(), 0) + + def test_related_object(self): + public_school = School.objects.create(is_public=True) + public_student = Student.objects.create(school=public_school) + + private_school = School.objects.create(is_public=False) + private_student = Student.objects.create(school=private_school) + + # Only one school is available via all() due to the custom default manager. + self.assertQuerysetEqual( + School.objects.all(), + [""] + ) + + self.assertEqual(public_student.school, public_school) + + # Make sure the base manager is used so that an student can still access + # its related school even if the default manager doesn't normally + # allow it. + self.assertEqual(private_student.school, private_school) + + # If the manager is marked "use_for_related_fields", it'll get used instead + # of the "bare" queryset. Usually you'd define this as a property on the class, + # but this approximates that in a way that's easier in tests. + School.objects.use_for_related_fields = True + try: + private_student = Student.objects.get(pk=private_student.pk) + self.assertRaises(School.DoesNotExist, lambda: private_student.school) + finally: + School.objects.use_for_related_fields = False + + def test_hasattr_related_object(self): + # The exception raised on attribute access when a related object + # doesn't exist should be an instance of a subclass of `AttributeError` + # refs #21563 + self.assertFalse(hasattr(Article(), 'reporter')) diff --git a/tests/many_to_one_null/models.py b/tests/many_to_one_null/models.py index 16ee56cec65..3b03c421059 100644 --- a/tests/many_to_one_null/models.py +++ b/tests/many_to_one_null/models.py @@ -27,3 +27,11 @@ class Article(models.Model): def __str__(self): return self.headline + + +class Car(models.Model): + make = models.CharField(max_length=100, null=True, unique=True) + + +class Driver(models.Model): + car = models.ForeignKey(Car, to_field='make', null=True, related_name='drivers') diff --git a/tests/many_to_one_null/tests.py b/tests/many_to_one_null/tests.py index 2463065c9d5..a72b821f88f 100644 --- a/tests/many_to_one_null/tests.py +++ b/tests/many_to_one_null/tests.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase -from .models import Reporter, Article +from .models import Article, Car, Driver, Reporter class ManyToOneNullTests(TestCase): @@ -105,3 +105,10 @@ class ManyToOneNullTests(TestCase): with self.assertNumQueries(1): r.article_set.clear() self.assertEqual(r.article_set.count(), 0) + + def test_related_null_to_field(self): + c1 = Car.objects.create() + d1 = Driver.objects.create() + self.assertIs(d1.car, None) + with self.assertNumQueries(0): + self.assertEqual(list(c1.drivers.all()), []) diff --git a/tests/many_to_one_regress/__init__.py b/tests/many_to_one_regress/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/many_to_one_regress/models.py b/tests/many_to_one_regress/models.py deleted file mode 100644 index f1239dbef2d..00000000000 --- a/tests/many_to_one_regress/models.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Regression tests for a few ForeignKey bugs. -""" -from __future__ import unicode_literals - -from django.db import models -from django.utils.encoding import python_2_unicode_compatible - -# If ticket #1578 ever slips back in, these models will not be able to be -# created (the field names being lower-cased versions of their opposite -# classes is important here). - - -class First(models.Model): - second = models.IntegerField() - - -class Second(models.Model): - first = models.ForeignKey(First, related_name='the_first') - - -# Protect against repetition of #1839, #2415 and #2536. -class Third(models.Model): - name = models.CharField(max_length=20) - third = models.ForeignKey('self', null=True, related_name='child_set') - - -class Parent(models.Model): - name = models.CharField(max_length=20, unique=True) - bestchild = models.ForeignKey('Child', null=True, related_name='favored_by') - - -class Child(models.Model): - name = models.CharField(max_length=20) - parent = models.ForeignKey(Parent) - - -class ToFieldChild(models.Model): - parent = models.ForeignKey(Parent, to_field='name') - - -# Multiple paths to the same model (#7110, #7125) -@python_2_unicode_compatible -class Category(models.Model): - name = models.CharField(max_length=20) - - def __str__(self): - return self.name - - -class Record(models.Model): - category = models.ForeignKey(Category) - - -@python_2_unicode_compatible -class Relation(models.Model): - left = models.ForeignKey(Record, related_name='left_set') - right = models.ForeignKey(Record, related_name='right_set') - - def __str__(self): - return "%s - %s" % (self.left.category.name, self.right.category.name) - - -class Car(models.Model): - make = models.CharField(max_length=100, null=True, unique=True) - - -class Driver(models.Model): - car = models.ForeignKey(Car, to_field='make', null=True, related_name='drivers') diff --git a/tests/many_to_one_regress/tests.py b/tests/many_to_one_regress/tests.py deleted file mode 100644 index f8e4d963636..00000000000 --- a/tests/many_to_one_regress/tests.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import unicode_literals - -from django.db import models -from django.test import TestCase - -from .models import ( - First, Third, Parent, Child, ToFieldChild, Category, Record, Relation, Car, Driver) - - -class ManyToOneRegressionTests(TestCase): - def test_object_creation(self): - Third.objects.create(id='3', name='An example') - parent = Parent(name='fred') - parent.save() - Child.objects.create(name='bam-bam', parent=parent) - - def test_fk_assignment_and_related_object_cache(self): - # Tests of ForeignKey assignment and the related-object cache (see #6886). - - p = Parent.objects.create(name="Parent") - c = Child.objects.create(name="Child", parent=p) - - # Look up the object again so that we get a "fresh" object. - c = Child.objects.get(name="Child") - p = c.parent - - # Accessing the related object again returns the exactly same object. - self.assertTrue(c.parent is p) - - # But if we kill the cache, we get a new object. - del c._parent_cache - self.assertFalse(c.parent is p) - - # Assigning a new object results in that object getting cached immediately. - p2 = Parent.objects.create(name="Parent 2") - c.parent = p2 - self.assertTrue(c.parent is p2) - - # Assigning None succeeds if field is null=True. - p.bestchild = None - self.assertTrue(p.bestchild is None) - - # bestchild should still be None after saving. - p.save() - self.assertTrue(p.bestchild is None) - - # bestchild should still be None after fetching the object again. - p = Parent.objects.get(name="Parent") - self.assertTrue(p.bestchild is None) - - # Assigning None fails: Child.parent is null=False. - self.assertRaises(ValueError, setattr, c, "parent", None) - - # You also can't assign an object of the wrong type here - self.assertRaises(ValueError, setattr, c, "parent", First(id=1, second=1)) - - # Nor can you explicitly assign None to Child.parent during object - # creation (regression for #9649). - self.assertRaises(ValueError, Child, name='xyzzy', parent=None) - self.assertRaises(ValueError, Child.objects.create, name='xyzzy', parent=None) - - # Creation using keyword argument should cache the related object. - p = Parent.objects.get(name="Parent") - c = Child(parent=p) - self.assertTrue(c.parent is p) - - # Creation using keyword argument and unsaved related instance (#8070). - p = Parent() - with self.assertRaisesMessage(ValueError, - 'Cannot assign "%r": "%s" instance isn\'t saved in the database.' - % (p, Child.parent.field.rel.to._meta.object_name)): - Child(parent=p) - - with self.assertRaisesMessage(ValueError, - 'Cannot assign "%r": "%s" instance isn\'t saved in the database.' - % (p, Child.parent.field.rel.to._meta.object_name)): - ToFieldChild(parent=p) - - # Creation using attname keyword argument and an id will cause the - # related object to be fetched. - p = Parent.objects.get(name="Parent") - c = Child(parent_id=p.id) - self.assertFalse(c.parent is p) - self.assertEqual(c.parent, p) - - def test_multiple_foreignkeys(self): - # Test of multiple ForeignKeys to the same model (bug #7125). - c1 = Category.objects.create(name='First') - c2 = Category.objects.create(name='Second') - c3 = Category.objects.create(name='Third') - r1 = Record.objects.create(category=c1) - r2 = Record.objects.create(category=c1) - r3 = Record.objects.create(category=c2) - r4 = Record.objects.create(category=c2) - r5 = Record.objects.create(category=c3) - Relation.objects.create(left=r1, right=r2) - Relation.objects.create(left=r3, right=r4) - Relation.objects.create(left=r1, right=r3) - Relation.objects.create(left=r5, right=r2) - Relation.objects.create(left=r3, right=r2) - - q1 = Relation.objects.filter(left__category__name__in=['First'], right__category__name__in=['Second']) - self.assertQuerysetEqual(q1, [""]) - - q2 = Category.objects.filter(record__left_set__right__category__name='Second').order_by('name') - self.assertQuerysetEqual(q2, ["", ""]) - - p = Parent.objects.create(name="Parent") - c = Child.objects.create(name="Child", parent=p) - self.assertRaises(ValueError, Child.objects.create, name="Grandchild", parent=c) - - def test_fk_instantiation_outside_model(self): - # Regression for #12190 -- Should be able to instantiate a FK outside - # of a model, and interrogate its related field. - cat = models.ForeignKey(Category) - self.assertEqual('id', cat.rel.get_related_field().name) - - def test_relation_unsaved(self): - # Test that the _set manager does not join on Null value fields (#17541) - Third.objects.create(name='Third 1') - Third.objects.create(name='Third 2') - th = Third(name="testing") - # The object isn't saved an thus the relation field is null - we won't even - # execute a query in this case. - with self.assertNumQueries(0): - self.assertEqual(th.child_set.count(), 0) - th.save() - # Now the model is saved, so we will need to execute an query. - with self.assertNumQueries(1): - self.assertEqual(th.child_set.count(), 0) - - def test_related_null_to_field(self): - c1 = Car.objects.create() - d1 = Driver.objects.create() - self.assertIs(d1.car, None) - with self.assertNumQueries(0): - self.assertEqual(list(c1.drivers.all()), []) diff --git a/tests/reverse_single_related/__init__.py b/tests/reverse_single_related/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/reverse_single_related/models.py b/tests/reverse_single_related/models.py deleted file mode 100644 index c7ec2edc6bb..00000000000 --- a/tests/reverse_single_related/models.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models - - -class SourceManager(models.Manager): - def get_queryset(self): - return super(SourceManager, self).get_queryset().filter(is_public=True) - - -class Source(models.Model): - is_public = models.BooleanField(default=False) - objects = SourceManager() - - -class Item(models.Model): - source = models.ForeignKey(Source) diff --git a/tests/reverse_single_related/tests.py b/tests/reverse_single_related/tests.py deleted file mode 100644 index 2a8b96ddbc9..00000000000 --- a/tests/reverse_single_related/tests.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.test import TestCase - -from .models import Source, Item - - -class ReverseSingleRelatedTests(TestCase): - """ - Regression tests for an object that cannot access a single related - object due to a restrictive default manager. - """ - - def test_reverse_single_related(self): - - public_source = Source.objects.create(is_public=True) - public_item = Item.objects.create(source=public_source) - - private_source = Source.objects.create(is_public=False) - private_item = Item.objects.create(source=private_source) - - # Only one source is available via all() due to the custom default manager. - self.assertQuerysetEqual( - Source.objects.all(), - [""] - ) - - self.assertEqual(public_item.source, public_source) - - # Make sure that an item can still access its related source even if the default - # manager doesn't normally allow it. - self.assertEqual(private_item.source, private_source) - - # If the manager is marked "use_for_related_fields", it'll get used instead - # of the "bare" queryset. Usually you'd define this as a property on the class, - # but this approximates that in a way that's easier in tests. - Source.objects.use_for_related_fields = True - try: - private_item = Item.objects.get(pk=private_item.pk) - self.assertRaises(Source.DoesNotExist, lambda: private_item.source) - finally: - Source.objects.use_for_related_fields = False - - def test_hasattr_single_related(self): - # The exception raised on attribute access when a related object - # doesn't exist should be an instance of a subclass of `AttributeError` - # refs #21563 - self.assertFalse(hasattr(Item(), 'source'))