123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161 |
- from datetime import date
- from decimal import Decimal
- from unittest import mock
- from django.db import connection, transaction
- from django.db.models import (
- BooleanField,
- Case,
- Count,
- DecimalField,
- ExpressionWrapper,
- F,
- FilteredRelation,
- Q,
- Sum,
- Value,
- When,
- )
- from django.db.models.functions import Concat
- from django.db.models.lookups import Exact, IStartsWith
- from django.test import TestCase
- from django.test.testcases import skipUnlessDBFeature
- from .models import (
- Author,
- Book,
- BookDailySales,
- Borrower,
- Currency,
- Editor,
- ExchangeRate,
- RentalSession,
- Reservation,
- Seller,
- )
- class FilteredRelationTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.author1 = Author.objects.create(name="Alice")
- cls.author2 = Author.objects.create(name="Jane")
- cls.editor_a = Editor.objects.create(name="a")
- cls.editor_b = Editor.objects.create(name="b")
- cls.book1 = Book.objects.create(
- title="Poem by Alice",
- editor=cls.editor_a,
- author=cls.author1,
- )
- cls.book1.generic_author.set([cls.author2])
- cls.book2 = Book.objects.create(
- title="The book by Jane A",
- editor=cls.editor_b,
- author=cls.author2,
- )
- cls.book3 = Book.objects.create(
- title="The book by Jane B",
- editor=cls.editor_b,
- author=cls.author2,
- )
- cls.book4 = Book.objects.create(
- title="The book by Alice",
- editor=cls.editor_a,
- author=cls.author1,
- )
- cls.author1.favorite_books.add(cls.book2)
- cls.author1.favorite_books.add(cls.book3)
- def test_select_related(self):
- qs = (
- Author.objects.annotate(
- book_join=FilteredRelation("book"),
- )
- .select_related("book_join__editor")
- .order_by("pk", "book_join__pk")
- )
- with self.assertNumQueries(1):
- self.assertQuerySetEqual(
- qs,
- [
- (self.author1, self.book1, self.editor_a, self.author1),
- (self.author1, self.book4, self.editor_a, self.author1),
- (self.author2, self.book2, self.editor_b, self.author2),
- (self.author2, self.book3, self.editor_b, self.author2),
- ],
- lambda x: (x, x.book_join, x.book_join.editor, x.book_join.author),
- )
- def test_select_related_multiple(self):
- qs = (
- Book.objects.annotate(
- author_join=FilteredRelation("author"),
- editor_join=FilteredRelation("editor"),
- )
- .select_related("author_join", "editor_join")
- .order_by("pk")
- )
- self.assertQuerySetEqual(
- qs,
- [
- (self.book1, self.author1, self.editor_a),
- (self.book2, self.author2, self.editor_b),
- (self.book3, self.author2, self.editor_b),
- (self.book4, self.author1, self.editor_a),
- ],
- lambda x: (x, x.author_join, x.editor_join),
- )
- def test_select_related_with_empty_relation(self):
- qs = (
- Author.objects.annotate(
- book_join=FilteredRelation("book", condition=Q(pk=-1)),
- )
- .select_related("book_join")
- .order_by("pk")
- )
- self.assertSequenceEqual(qs, [self.author1, self.author2])
- def test_select_related_foreign_key(self):
- qs = (
- Book.objects.annotate(
- author_join=FilteredRelation("author"),
- )
- .select_related("author_join")
- .order_by("pk")
- )
- with self.assertNumQueries(1):
- self.assertQuerySetEqual(
- qs,
- [
- (self.book1, self.author1),
- (self.book2, self.author2),
- (self.book3, self.author2),
- (self.book4, self.author1),
- ],
- lambda x: (x, x.author_join),
- )
- @skipUnlessDBFeature("has_select_for_update", "has_select_for_update_of")
- def test_select_related_foreign_key_for_update_of(self):
- with transaction.atomic():
- qs = (
- Book.objects.annotate(
- author_join=FilteredRelation("author"),
- )
- .select_related("author_join")
- .select_for_update(of=("self",))
- .order_by("pk")
- )
- with self.assertNumQueries(1):
- self.assertQuerySetEqual(
- qs,
- [
- (self.book1, self.author1),
- (self.book2, self.author2),
- (self.book3, self.author2),
- (self.book4, self.author1),
- ],
- lambda x: (x, x.author_join),
- )
- def test_without_join(self):
- self.assertCountEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ),
- [self.author1, self.author2],
- )
- def test_with_join(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False),
- [self.author1],
- )
- def test_with_exclude(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).exclude(book_alice__isnull=False),
- [self.author2],
- )
- def test_with_join_and_complex_condition(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book",
- condition=Q(
- Q(book__title__iexact="poem by alice")
- | Q(book__state=Book.RENTED)
- ),
- ),
- ).filter(book_alice__isnull=False),
- [self.author1],
- )
- def test_internal_queryset_alias_mapping(self):
- queryset = Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False)
- self.assertIn(
- "INNER JOIN {} book_alice ON".format(
- connection.ops.quote_name("filtered_relation_book")
- ),
- str(queryset.query),
- )
- def test_multiple(self):
- qs = (
- Author.objects.annotate(
- book_title_alice=FilteredRelation(
- "book", condition=Q(book__title__contains="Alice")
- ),
- book_title_jane=FilteredRelation(
- "book", condition=Q(book__title__icontains="Jane")
- ),
- )
- .filter(name="Jane")
- .values("book_title_alice__title", "book_title_jane__title")
- )
- empty = "" if connection.features.interprets_empty_strings_as_nulls else None
- self.assertCountEqual(
- qs,
- [
- {
- "book_title_alice__title": empty,
- "book_title_jane__title": "The book by Jane A",
- },
- {
- "book_title_alice__title": empty,
- "book_title_jane__title": "The book by Jane B",
- },
- ],
- )
- def test_with_multiple_filter(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_editor_a=FilteredRelation(
- "book",
- condition=Q(
- book__title__icontains="book", book__editor_id=self.editor_a.pk
- ),
- ),
- ).filter(book_editor_a__isnull=False),
- [self.author1],
- )
- def test_multiple_times(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_title_alice=FilteredRelation(
- "book", condition=Q(book__title__icontains="alice")
- ),
- )
- .filter(book_title_alice__isnull=False)
- .filter(book_title_alice__isnull=False)
- .distinct(),
- [self.author1],
- )
- def test_exclude_relation_with_join(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=~Q(book__title__icontains="alice")
- ),
- )
- .filter(book_alice__isnull=False)
- .distinct(),
- [self.author2],
- )
- def test_with_m2m(self):
- qs = Author.objects.annotate(
- favorite_books_written_by_jane=FilteredRelation(
- "favorite_books",
- condition=Q(favorite_books__in=[self.book2]),
- ),
- ).filter(favorite_books_written_by_jane__isnull=False)
- self.assertSequenceEqual(qs, [self.author1])
- def test_with_m2m_deep(self):
- qs = Author.objects.annotate(
- favorite_books_written_by_jane=FilteredRelation(
- "favorite_books",
- condition=Q(favorite_books__author=self.author2),
- ),
- ).filter(favorite_books_written_by_jane__title="The book by Jane B")
- self.assertSequenceEqual(qs, [self.author1])
- def test_with_m2m_multijoin(self):
- qs = (
- Author.objects.annotate(
- favorite_books_written_by_jane=FilteredRelation(
- "favorite_books",
- condition=Q(favorite_books__author=self.author2),
- )
- )
- .filter(favorite_books_written_by_jane__editor__name="b")
- .distinct()
- )
- self.assertSequenceEqual(qs, [self.author1])
- def test_values_list(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- )
- .filter(book_alice__isnull=False)
- .values_list("book_alice__title", flat=True),
- ["Poem by Alice"],
- )
- def test_values(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- )
- .filter(book_alice__isnull=False)
- .values(),
- [
- {
- "id": self.author1.pk,
- "name": "Alice",
- "content_type_id": None,
- "object_id": None,
- }
- ],
- )
- def test_extra(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- )
- .filter(book_alice__isnull=False)
- .extra(where=["1 = 1"]),
- [self.author1],
- )
- @skipUnlessDBFeature("supports_select_union")
- def test_union(self):
- qs1 = Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False)
- qs2 = Author.objects.annotate(
- book_jane=FilteredRelation(
- "book", condition=Q(book__title__iexact="the book by jane a")
- ),
- ).filter(book_jane__isnull=False)
- self.assertSequenceEqual(qs1.union(qs2), [self.author1, self.author2])
- @skipUnlessDBFeature("supports_select_intersection")
- def test_intersection(self):
- qs1 = Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False)
- qs2 = Author.objects.annotate(
- book_jane=FilteredRelation(
- "book", condition=Q(book__title__iexact="the book by jane a")
- ),
- ).filter(book_jane__isnull=False)
- self.assertSequenceEqual(qs1.intersection(qs2), [])
- @skipUnlessDBFeature("supports_select_difference")
- def test_difference(self):
- qs1 = Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False)
- qs2 = Author.objects.annotate(
- book_jane=FilteredRelation(
- "book", condition=Q(book__title__iexact="the book by jane a")
- ),
- ).filter(book_jane__isnull=False)
- self.assertSequenceEqual(qs1.difference(qs2), [self.author1])
- def test_select_for_update(self):
- self.assertSequenceEqual(
- Author.objects.annotate(
- book_jane=FilteredRelation(
- "book", condition=Q(book__title__iexact="the book by jane a")
- ),
- )
- .filter(book_jane__isnull=False)
- .select_for_update(),
- [self.author2],
- )
- def test_defer(self):
- # One query for the list and one query for the deferred title.
- with self.assertNumQueries(2):
- self.assertQuerySetEqual(
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- )
- .filter(book_alice__isnull=False)
- .select_related("book_alice")
- .defer("book_alice__title"),
- ["Poem by Alice"],
- lambda author: author.book_alice.title,
- )
- def test_only_not_supported(self):
- msg = "only() is not supported with FilteredRelation."
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False).select_related("book_alice").only(
- "book_alice__state"
- )
- def test_as_subquery(self):
- inner_qs = Author.objects.annotate(
- book_alice=FilteredRelation(
- "book", condition=Q(book__title__iexact="poem by alice")
- ),
- ).filter(book_alice__isnull=False)
- qs = Author.objects.filter(id__in=inner_qs)
- self.assertSequenceEqual(qs, [self.author1])
- def test_nested_foreign_key(self):
- qs = (
- Author.objects.annotate(
- book_editor_worked_with=FilteredRelation(
- "book__editor",
- condition=Q(book__title__icontains="book by"),
- ),
- )
- .filter(
- book_editor_worked_with__isnull=False,
- )
- .select_related(
- "book_editor_worked_with",
- )
- .order_by("pk", "book_editor_worked_with__pk")
- )
- with self.assertNumQueries(1):
- self.assertQuerySetEqual(
- qs,
- [
- (self.author1, self.editor_a),
- (self.author2, self.editor_b),
- (self.author2, self.editor_b),
- ],
- lambda x: (x, x.book_editor_worked_with),
- )
- def test_nested_foreign_key_nested_field(self):
- qs = (
- Author.objects.annotate(
- book_editor_worked_with=FilteredRelation(
- "book__editor", condition=Q(book__title__icontains="book by")
- ),
- )
- .filter(
- book_editor_worked_with__isnull=False,
- )
- .values(
- "name",
- "book_editor_worked_with__name",
- )
- .order_by("name", "book_editor_worked_with__name")
- .distinct()
- )
- self.assertSequenceEqual(
- qs,
- [
- {
- "name": self.author1.name,
- "book_editor_worked_with__name": self.editor_a.name,
- },
- {
- "name": self.author2.name,
- "book_editor_worked_with__name": self.editor_b.name,
- },
- ],
- )
- def test_nested_foreign_key_filtered_base_object(self):
- qs = (
- Author.objects.annotate(
- alice_editors=FilteredRelation(
- "book__editor",
- condition=Q(name="Alice"),
- ),
- )
- .values(
- "name",
- "alice_editors__pk",
- )
- .order_by("name", "alice_editors__name")
- .distinct()
- )
- self.assertSequenceEqual(
- qs,
- [
- {"name": self.author1.name, "alice_editors__pk": self.editor_a.pk},
- {"name": self.author2.name, "alice_editors__pk": None},
- ],
- )
- def test_nested_m2m_filtered(self):
- qs = (
- Book.objects.annotate(
- favorite_book=FilteredRelation(
- "author__favorite_books",
- condition=Q(author__favorite_books__title__icontains="book by"),
- ),
- )
- .values(
- "title",
- "favorite_book__pk",
- )
- .order_by("title", "favorite_book__title")
- )
- self.assertSequenceEqual(
- qs,
- [
- {"title": self.book1.title, "favorite_book__pk": self.book2.pk},
- {"title": self.book1.title, "favorite_book__pk": self.book3.pk},
- {"title": self.book4.title, "favorite_book__pk": self.book2.pk},
- {"title": self.book4.title, "favorite_book__pk": self.book3.pk},
- {"title": self.book2.title, "favorite_book__pk": None},
- {"title": self.book3.title, "favorite_book__pk": None},
- ],
- )
- def test_nested_chained_relations(self):
- qs = (
- Author.objects.annotate(
- my_books=FilteredRelation(
- "book",
- condition=Q(book__title__icontains="book by"),
- ),
- preferred_by_authors=FilteredRelation(
- "my_books__preferred_by_authors",
- condition=Q(my_books__preferred_by_authors__name="Alice"),
- ),
- )
- .annotate(
- author=F("name"),
- book_title=F("my_books__title"),
- preferred_by_author_pk=F("preferred_by_authors"),
- )
- .order_by("author", "book_title", "preferred_by_author_pk")
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Alice", "The book by Alice", None),
- ("Jane", "The book by Jane A", self.author1.pk),
- ("Jane", "The book by Jane B", self.author1.pk),
- ],
- lambda x: (x.author, x.book_title, x.preferred_by_author_pk),
- )
- def test_three_level_nested_chained_relations(self):
- borrower = Borrower.objects.create(name="Jenny")
- Reservation.objects.create(
- borrower=borrower,
- book=self.book1,
- state=Reservation.STOPPED,
- )
- qs = Author.objects.annotate(
- my_books=FilteredRelation("book"),
- my_reserved_books=FilteredRelation(
- "my_books__reservation",
- condition=Q(my_books__reservation__state=Reservation.STOPPED),
- ),
- my_readers=FilteredRelation(
- "my_reserved_books__borrower",
- condition=Q(my_reserved_books__borrower=borrower),
- ),
- )
- self.assertSequenceEqual(
- qs.filter(my_readers=borrower).values_list("name", flat=True), ["Alice"]
- )
- def test_deep_nested_foreign_key(self):
- qs = (
- Book.objects.annotate(
- author_favorite_book_editor=FilteredRelation(
- "author__favorite_books__editor",
- condition=Q(author__favorite_books__title__icontains="Jane A"),
- ),
- )
- .filter(
- author_favorite_book_editor__isnull=False,
- )
- .select_related(
- "author_favorite_book_editor",
- )
- .order_by("pk", "author_favorite_book_editor__pk")
- )
- with self.assertNumQueries(1):
- self.assertQuerySetEqual(
- qs,
- [
- (self.book1, self.editor_b),
- (self.book4, self.editor_b),
- ],
- lambda x: (x, x.author_favorite_book_editor),
- )
- def test_relation_name_lookup(self):
- msg = (
- "FilteredRelation's relation_name cannot contain lookups (got "
- "'book__title__icontains')."
- )
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- book_title=FilteredRelation(
- "book__title__icontains",
- condition=Q(book__title="Poem by Alice"),
- ),
- )
- def test_condition_outside_relation_name(self):
- msg = (
- "FilteredRelation's condition doesn't support relations outside "
- "the 'book__editor' (got 'book__author__name__icontains')."
- )
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- book_editor=FilteredRelation(
- "book__editor",
- condition=Q(book__author__name__icontains="book"),
- ),
- )
- def test_condition_with_exact_lookup_outside_relation_name(self):
- qs = Author.objects.annotate(
- book_editor=FilteredRelation(
- "book__editor",
- condition=Q(book__author__name="book"),
- ),
- ).filter(book_editor__isnull=True)
- self.assertEqual(qs.count(), 4)
- def test_condition_with_func_and_lookup_outside_relation_name(self):
- qs = Author.objects.annotate(
- book_editor=FilteredRelation(
- "book__editor",
- condition=Q(
- book__title=Concat(Value("The book by "), F("book__author__name"))
- ),
- ),
- ).filter(book_editor__isnull=False)
- self.assertEqual(qs.count(), 1)
- def test_condition_deeper_relation_name(self):
- msg = (
- "FilteredRelation's condition doesn't support nested relations "
- "deeper than the relation_name (got "
- "'book__editor__name__icontains' for 'book')."
- )
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- book_editor=FilteredRelation(
- "book",
- condition=Q(book__editor__name__icontains="b"),
- ),
- )
- def test_condition_deeper_relation_name_implicit_exact(self):
- msg = (
- "FilteredRelation's condition doesn't support nested relations "
- "deeper than the relation_name (got 'book__editor__name' for 'book')."
- )
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- book_editor=FilteredRelation(
- "book",
- condition=Q(book__editor__name="b"),
- ),
- )
- def test_with_empty_relation_name_error(self):
- with self.assertRaisesMessage(ValueError, "relation_name cannot be empty."):
- FilteredRelation("", condition=Q(blank=""))
- def test_with_condition_as_expression_error(self):
- msg = "condition argument must be a Q() instance."
- expression = Case(
- When(book__title__iexact="poem by alice", then=True),
- default=False,
- )
- with self.assertRaisesMessage(ValueError, msg):
- FilteredRelation("book", condition=expression)
- def test_with_prefetch_related(self):
- msg = "prefetch_related() is not supported with FilteredRelation."
- qs = Author.objects.annotate(
- book_title_contains_b=FilteredRelation(
- "book", condition=Q(book__title__icontains="b")
- ),
- ).filter(
- book_title_contains_b__isnull=False,
- )
- with self.assertRaisesMessage(ValueError, msg):
- qs.prefetch_related("book_title_contains_b")
- with self.assertRaisesMessage(ValueError, msg):
- qs.prefetch_related("book_title_contains_b__editor")
- def test_with_generic_foreign_key(self):
- self.assertSequenceEqual(
- Book.objects.annotate(
- generic_authored_book=FilteredRelation(
- "generic_author", condition=Q(generic_author__isnull=False)
- ),
- ).filter(generic_authored_book__isnull=False),
- [self.book1],
- )
- def test_eq(self):
- self.assertEqual(
- FilteredRelation("book", condition=Q(book__title="b")), mock.ANY
- )
- def test_conditional_expression(self):
- qs = Author.objects.annotate(
- the_book=FilteredRelation("book", condition=Q(Value(False))),
- ).filter(the_book__isnull=False)
- self.assertSequenceEqual(qs, [])
- def test_expression_outside_relation_name(self):
- qs = Author.objects.annotate(
- book_editor=FilteredRelation(
- "book__editor",
- condition=Q(
- Exact(F("book__author__name"), "Alice"),
- Value(True),
- book__title__startswith="Poem",
- ),
- ),
- ).filter(book_editor__isnull=False)
- self.assertSequenceEqual(qs, [self.author1])
- def test_conditional_expression_with_case(self):
- qs = Book.objects.annotate(
- alice_author=FilteredRelation(
- "author",
- condition=Q(
- Case(When(author__name="Alice", then=True), default=False),
- ),
- ),
- ).filter(alice_author__isnull=False)
- self.assertCountEqual(qs, [self.book1, self.book4])
- def test_conditional_expression_outside_relation_name(self):
- tests = [
- Q(Case(When(book__author__name="Alice", then=True), default=False)),
- Q(
- ExpressionWrapper(
- Q(Value(True), Exact(F("book__author__name"), "Alice")),
- output_field=BooleanField(),
- ),
- ),
- ]
- for condition in tests:
- with self.subTest(condition=condition):
- qs = Author.objects.annotate(
- book_editor=FilteredRelation("book__editor", condition=condition),
- ).filter(book_editor__isnull=True)
- self.assertSequenceEqual(qs, [self.author2, self.author2])
- def test_conditional_expression_with_lookup(self):
- lookups = [
- Q(book__title__istartswith="poem"),
- Q(IStartsWith(F("book__title"), "poem")),
- ]
- for condition in lookups:
- with self.subTest(condition=condition):
- qs = Author.objects.annotate(
- poem_book=FilteredRelation("book", condition=condition)
- ).filter(poem_book__isnull=False)
- self.assertSequenceEqual(qs, [self.author1])
- def test_conditional_expression_with_expressionwrapper(self):
- qs = Author.objects.annotate(
- poem_book=FilteredRelation(
- "book",
- condition=Q(
- ExpressionWrapper(
- Q(Exact(F("book__title"), "Poem by Alice")),
- output_field=BooleanField(),
- ),
- ),
- ),
- ).filter(poem_book__isnull=False)
- self.assertSequenceEqual(qs, [self.author1])
- def test_conditional_expression_with_multiple_fields(self):
- qs = Author.objects.annotate(
- my_books=FilteredRelation(
- "book__author",
- condition=Q(Exact(F("book__author__name"), F("book__author__name"))),
- ),
- ).filter(my_books__isnull=True)
- self.assertSequenceEqual(qs, [])
- def test_conditional_expression_rhs_contains_relation_name(self):
- qs = Book.objects.annotate(
- rel=FilteredRelation(
- "editor",
- condition=Q(id=1 * F("number_editor")),
- )
- ).filter(rel__isnull=True)
- self.assertSequenceEqual(qs, [])
- def test_conditional_expression_rhs_startswith_relation_name(self):
- qs = Book.objects.annotate(
- rel=FilteredRelation(
- "editor",
- condition=Q(id=1 * F("editor_number")),
- )
- ).filter(rel__isnull=True)
- self.assertSequenceEqual(qs, [])
- def test_conditional_expression_lhs_startswith_relation_name(self):
- qs = Book.objects.annotate(
- rel=FilteredRelation(
- "editor",
- condition=Q(editor_number__gt=1),
- )
- ).filter(rel__isnull=True)
- self.assertSequenceEqual(qs, [])
- def test_conditional_expression_lhs_contains_relation_name(self):
- qs = Book.objects.annotate(
- rel=FilteredRelation(
- "editor",
- condition=Q(number_editor__gt=1),
- )
- ).filter(rel__isnull=True)
- self.assertSequenceEqual(qs, [])
- def test_conditional_expression_does_not_support_queryset(self):
- msg = "Passing a QuerySet within a FilteredRelation is not supported."
- with self.assertRaisesMessage(ValueError, msg):
- Author.objects.annotate(
- poem_book=FilteredRelation(
- "book",
- condition=Q(book__in=Book.objects.filter(title__istartswith="a")),
- ),
- ).filter(poem_book__isnull=False)
- class FilteredRelationAggregationTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- cls.author1 = Author.objects.create(name="Alice")
- cls.editor_a = Editor.objects.create(name="a")
- cls.book1 = Book.objects.create(
- title="Poem by Alice",
- editor=cls.editor_a,
- author=cls.author1,
- )
- cls.borrower1 = Borrower.objects.create(name="Jenny")
- cls.borrower2 = Borrower.objects.create(name="Kevin")
- # borrower 1 reserves, rents, and returns book1.
- Reservation.objects.create(
- borrower=cls.borrower1,
- book=cls.book1,
- state=Reservation.STOPPED,
- )
- RentalSession.objects.create(
- borrower=cls.borrower1,
- book=cls.book1,
- state=RentalSession.STOPPED,
- )
- # borrower2 reserves, rents, and returns book1.
- Reservation.objects.create(
- borrower=cls.borrower2,
- book=cls.book1,
- state=Reservation.STOPPED,
- )
- RentalSession.objects.create(
- borrower=cls.borrower2,
- book=cls.book1,
- state=RentalSession.STOPPED,
- )
- def test_aggregate(self):
- """
- filtered_relation() not only improves performance but also creates
- correct results when aggregating with multiple LEFT JOINs.
- Books can be reserved then rented by a borrower. Each reservation and
- rental session are recorded with Reservation and RentalSession models.
- Every time a reservation or a rental session is over, their state is
- changed to 'stopped'.
- Goal: Count number of books that are either currently reserved or
- rented by borrower1 or available.
- """
- qs = (
- Book.objects.annotate(
- is_reserved_or_rented_by=Case(
- When(
- reservation__state=Reservation.NEW,
- then=F("reservation__borrower__pk"),
- ),
- When(
- rental_session__state=RentalSession.NEW,
- then=F("rental_session__borrower__pk"),
- ),
- default=None,
- )
- )
- .filter(
- Q(is_reserved_or_rented_by=self.borrower1.pk) | Q(state=Book.AVAILABLE)
- )
- .distinct()
- )
- self.assertEqual(qs.count(), 1)
- # If count is equal to 1, the same aggregation should return in the
- # same result but it returns 4.
- self.assertSequenceEqual(
- qs.annotate(total=Count("pk")).values("total"), [{"total": 4}]
- )
- # With FilteredRelation, the result is as expected (1).
- qs = (
- Book.objects.annotate(
- active_reservations=FilteredRelation(
- "reservation",
- condition=Q(
- reservation__state=Reservation.NEW,
- reservation__borrower=self.borrower1,
- ),
- ),
- )
- .annotate(
- active_rental_sessions=FilteredRelation(
- "rental_session",
- condition=Q(
- rental_session__state=RentalSession.NEW,
- rental_session__borrower=self.borrower1,
- ),
- ),
- )
- .filter(
- (
- Q(active_reservations__isnull=False)
- | Q(active_rental_sessions__isnull=False)
- )
- | Q(state=Book.AVAILABLE)
- )
- .distinct()
- )
- self.assertEqual(qs.count(), 1)
- self.assertSequenceEqual(
- qs.annotate(total=Count("pk")).values("total"), [{"total": 1}]
- )
- def test_condition_spans_join(self):
- self.assertSequenceEqual(
- Book.objects.annotate(
- contains_editor_author=FilteredRelation(
- "author", condition=Q(author__name__icontains=F("editor__name"))
- )
- ).filter(
- contains_editor_author__isnull=False,
- ),
- [self.book1],
- )
- def test_condition_spans_join_chained(self):
- self.assertSequenceEqual(
- Book.objects.annotate(
- contains_editor_author=FilteredRelation(
- "author", condition=Q(author__name__icontains=F("editor__name"))
- ),
- contains_editor_author_ref=FilteredRelation(
- "author",
- condition=Q(author__name=F("contains_editor_author__name")),
- ),
- ).filter(
- contains_editor_author_ref__isnull=False,
- ),
- [self.book1],
- )
- def test_condition_self_ref(self):
- self.assertSequenceEqual(
- Book.objects.annotate(
- contains_author=FilteredRelation(
- "author",
- condition=Q(title__icontains=F("author__name")),
- )
- ).filter(
- contains_author__isnull=False,
- ),
- [self.book1],
- )
- class FilteredRelationAnalyticalAggregationTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- author = Author.objects.create(name="Author")
- editor = Editor.objects.create(name="Editor")
- cls.book1 = Book.objects.create(
- title="Poem by Alice",
- editor=editor,
- author=author,
- )
- cls.book2 = Book.objects.create(
- title="The book by Jane A",
- editor=editor,
- author=author,
- )
- cls.book3 = Book.objects.create(
- title="The book by Jane B",
- editor=editor,
- author=author,
- )
- cls.seller1 = Seller.objects.create(name="Seller 1")
- cls.seller2 = Seller.objects.create(name="Seller 2")
- cls.usd = Currency.objects.create(currency="USD")
- cls.eur = Currency.objects.create(currency="EUR")
- cls.sales_date1 = date(2020, 7, 6)
- cls.sales_date2 = date(2020, 7, 7)
- ExchangeRate.objects.bulk_create(
- [
- ExchangeRate(
- rate_date=cls.sales_date1,
- from_currency=cls.usd,
- to_currency=cls.eur,
- rate=0.40,
- ),
- ExchangeRate(
- rate_date=cls.sales_date1,
- from_currency=cls.eur,
- to_currency=cls.usd,
- rate=1.60,
- ),
- ExchangeRate(
- rate_date=cls.sales_date2,
- from_currency=cls.usd,
- to_currency=cls.eur,
- rate=0.50,
- ),
- ExchangeRate(
- rate_date=cls.sales_date2,
- from_currency=cls.eur,
- to_currency=cls.usd,
- rate=1.50,
- ),
- ExchangeRate(
- rate_date=cls.sales_date2,
- from_currency=cls.usd,
- to_currency=cls.usd,
- rate=1.00,
- ),
- ]
- )
- BookDailySales.objects.bulk_create(
- [
- BookDailySales(
- book=cls.book1,
- sale_date=cls.sales_date1,
- currency=cls.usd,
- sales=100.00,
- seller=cls.seller1,
- ),
- BookDailySales(
- book=cls.book2,
- sale_date=cls.sales_date1,
- currency=cls.eur,
- sales=200.00,
- seller=cls.seller1,
- ),
- BookDailySales(
- book=cls.book1,
- sale_date=cls.sales_date2,
- currency=cls.usd,
- sales=50.00,
- seller=cls.seller2,
- ),
- BookDailySales(
- book=cls.book2,
- sale_date=cls.sales_date2,
- currency=cls.eur,
- sales=100.00,
- seller=cls.seller2,
- ),
- ]
- )
- def test_aggregate(self):
- tests = [
- Q(daily_sales__sale_date__gte=self.sales_date2),
- ~Q(daily_sales__seller=self.seller1),
- ]
- for condition in tests:
- with self.subTest(condition=condition):
- qs = (
- Book.objects.annotate(
- recent_sales=FilteredRelation(
- "daily_sales", condition=condition
- ),
- recent_sales_rates=FilteredRelation(
- "recent_sales__currency__rates_from",
- condition=Q(
- recent_sales__currency__rates_from__rate_date=F(
- "recent_sales__sale_date"
- ),
- recent_sales__currency__rates_from__to_currency=(
- self.usd
- ),
- ),
- ),
- )
- .annotate(
- sales_sum=Sum(
- F("recent_sales__sales") * F("recent_sales_rates__rate"),
- output_field=DecimalField(),
- ),
- )
- .values("title", "sales_sum")
- .order_by(
- F("sales_sum").desc(nulls_last=True),
- )
- )
- self.assertSequenceEqual(
- qs,
- [
- {"title": self.book2.title, "sales_sum": Decimal(150.00)},
- {"title": self.book1.title, "sales_sum": Decimal(50.00)},
- {"title": self.book3.title, "sales_sum": None},
- ],
- )
|