diff options
author | Alex Hill <alex@hill.net.au> | 2015-07-10 15:52:22 +0800 |
---|---|---|
committer | Tim Graham <timograham@gmail.com> | 2015-08-15 07:26:44 -0400 |
commit | 98bcdfa8bd902addd4b8cf37d039b3597d58a45c (patch) | |
tree | 602c6833ece8dba33e62444ef76161d573d21430 /tests/foreign_object | |
parent | f9636fdf922fe49ff82d02b17d6b34469fcf1fda (diff) | |
download | django-98bcdfa8bd902addd4b8cf37d039b3597d58a45c.tar.gz |
Fixed #25064 -- Allowed empty join columns.
Diffstat (limited to 'tests/foreign_object')
-rw-r--r-- | tests/foreign_object/models/__init__.py | 3 | ||||
-rw-r--r-- | tests/foreign_object/models/empty_join.py | 100 | ||||
-rw-r--r-- | tests/foreign_object/test_empty_join.py | 47 |
3 files changed, 149 insertions, 1 deletions
diff --git a/tests/foreign_object/models/__init__.py b/tests/foreign_object/models/__init__.py index c7063ff387..8fe4ff10df 100644 --- a/tests/foreign_object/models/__init__.py +++ b/tests/foreign_object/models/__init__.py @@ -1,9 +1,10 @@ from .article import ( Article, ArticleIdea, ArticleTag, ArticleTranslation, NewsArticle, ) +from .empty_join import SlugPage from .person import Country, Friendship, Group, Membership, Person __all__ = [ 'Article', 'ArticleIdea', 'ArticleTag', 'ArticleTranslation', 'Country', - 'Friendship', 'Group', 'Membership', 'NewsArticle', 'Person', + 'Friendship', 'Group', 'Membership', 'NewsArticle', 'Person', 'SlugPage', ] diff --git a/tests/foreign_object/models/empty_join.py b/tests/foreign_object/models/empty_join.py new file mode 100644 index 0000000000..3bf121bf39 --- /dev/null +++ b/tests/foreign_object/models/empty_join.py @@ -0,0 +1,100 @@ +from django.db import models +from django.db.models.fields.related import ( + ForeignObjectRel, ForeignRelatedObjectsDescriptor, +) +from django.db.models.lookups import StartsWith +from django.db.models.query_utils import PathInfo +from django.utils.encoding import python_2_unicode_compatible + + +class CustomForeignObjectRel(ForeignObjectRel): + """ + Define some extra Field methods so this Rel acts more like a Field, which + lets us use ForeignRelatedObjectsDescriptor in both directions. + """ + @property + def foreign_related_fields(self): + return tuple(lhs_field for lhs_field, rhs_field in self.field.related_fields) + + def get_attname(self): + return self.name + + +class StartsWithRelation(models.ForeignObject): + """ + A ForeignObject that uses StartsWith operator in its joins instead of + the default equality operator. This is logically a many-to-many relation + and creates a ForeignRelatedObjectsDescriptor in both directions. + """ + auto_created = False + + many_to_many = False + many_to_one = True + one_to_many = False + one_to_one = False + + rel_class = CustomForeignObjectRel + + def __init__(self, *args, **kwargs): + kwargs['on_delete'] = models.DO_NOTHING + super(StartsWithRelation, self).__init__(*args, **kwargs) + + @property + def field(self): + """ + Makes ForeignRelatedObjectsDescriptor work in both directions. + """ + return self.remote_field + + def get_extra_restriction(self, where_class, alias, related_alias): + to_field = self.remote_field.model._meta.get_field(self.to_fields[0]) + from_field = self.model._meta.get_field(self.from_fields[0]) + return StartsWith(to_field.get_col(alias), from_field.get_col(related_alias)) + + def get_joining_columns(self, reverse_join=False): + return tuple() + + def get_path_info(self): + to_opts = self.remote_field.model._meta + from_opts = self.model._meta + return [PathInfo(from_opts, to_opts, (to_opts.pk,), self, False, False)] + + def get_reverse_path_info(self): + to_opts = self.model._meta + from_opts = self.remote_field.model._meta + return [PathInfo(from_opts, to_opts, (to_opts.pk,), self.remote_field, False, False)] + + def contribute_to_class(self, cls, name, virtual_only=False): + super(StartsWithRelation, self).contribute_to_class(cls, name, virtual_only) + setattr(cls, self.name, ForeignRelatedObjectsDescriptor(self)) + + +class BrokenContainsRelation(StartsWithRelation): + """ + This model is designed to yield no join conditions and + raise an exception in ``Join.as_sql()``. + """ + def get_extra_restriction(self, where_class, alias, related_alias): + return None + + +@python_2_unicode_compatible +class SlugPage(models.Model): + slug = models.CharField(max_length=20) + descendants = StartsWithRelation( + 'self', + from_fields=['slug'], + to_fields=['slug'], + related_name='ascendants', + ) + containers = BrokenContainsRelation( + 'self', + from_fields=['slug'], + to_fields=['slug'], + ) + + class Meta: + ordering = ['slug'] + + def __str__(self): + return 'SlugPage %s' % self.slug diff --git a/tests/foreign_object/test_empty_join.py b/tests/foreign_object/test_empty_join.py new file mode 100644 index 0000000000..a043595ce2 --- /dev/null +++ b/tests/foreign_object/test_empty_join.py @@ -0,0 +1,47 @@ +from django.test import TestCase + +from .models import SlugPage + + +class RestrictedConditionsTests(TestCase): + def setUp(self): + slugs = [ + 'a', + 'a/a', + 'a/b', + 'a/b/a', + 'x', + 'x/y/z', + ] + SlugPage.objects.bulk_create([SlugPage(slug=slug) for slug in slugs]) + + def test_restrictions_with_no_joining_columns(self): + """ + Test that it's possible to create a working related field that doesn't + use any joining columns, as long as an extra restriction is supplied. + """ + a = SlugPage.objects.get(slug='a') + self.assertListEqual( + [p.slug for p in SlugPage.objects.filter(ascendants=a)], + ['a', 'a/a', 'a/b', 'a/b/a'], + ) + self.assertEqual( + [p.slug for p in a.descendants.all()], + ['a', 'a/a', 'a/b', 'a/b/a'], + ) + + aba = SlugPage.objects.get(slug='a/b/a') + self.assertListEqual( + [p.slug for p in SlugPage.objects.filter(descendants__in=[aba])], + ['a', 'a/b', 'a/b/a'], + ) + self.assertListEqual( + [p.slug for p in aba.ascendants.all()], + ['a', 'a/b', 'a/b/a'], + ) + + def test_empty_join_conditions(self): + x = SlugPage.objects.get(slug='x') + message = "Join generated an empty ON clause." + with self.assertRaisesMessage(ValueError, message): + list(SlugPage.objects.filter(containers=x)) |