from datetime import date, datetime, timedelta from operator import attrgetter from django.db import IntegrityError from django.test import TestCase from .models import ( CustomMembership, Employee, Event, Friendship, Group, Ingredient, Invitation, Membership, Person, PersonChild, PersonSelfRefM2M, Recipe, RecipeIngredient, Relationship, SymmetricalFriendship, ) class M2mThroughTests(TestCase): @classmethod def setUpTestData(cls): cls.bob = Person.objects.create(name='Bob') cls.jim = Person.objects.create(name='Jim') cls.jane = Person.objects.create(name='Jane') cls.rock = Group.objects.create(name='Rock') cls.roll = Group.objects.create(name='Roll') def test_reverse_inherited_m2m_with_through_fields_list_hashable(self): reverse_m2m = Person._meta.get_field('events_invited') self.assertEqual(reverse_m2m.through_fields, ['event', 'invitee']) inherited_reverse_m2m = PersonChild._meta.get_field('events_invited') self.assertEqual(inherited_reverse_m2m.through_fields, ['event', 'invitee']) self.assertEqual(hash(reverse_m2m), hash(inherited_reverse_m2m)) def test_retrieve_intermediate_items(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jane, group=self.rock) expected = ['Jane', 'Jim'] self.assertQuerysetEqual( self.rock.members.all(), expected, attrgetter("name") ) def test_get_on_intermediate_model(self): Membership.objects.create(person=self.jane, group=self.rock) queryset = Membership.objects.get(person=self.jane, group=self.rock) self.assertEqual( repr(queryset), '' ) def test_filter_on_intermediate_model(self): m1 = Membership.objects.create(person=self.jim, group=self.rock) m2 = Membership.objects.create(person=self.jane, group=self.rock) queryset = Membership.objects.filter(group=self.rock) self.assertSequenceEqual(queryset, [m1, m2]) def test_add_on_m2m_with_intermediate_model(self): self.rock.members.add(self.bob, through_defaults={'invite_reason': 'He is good.'}) self.assertSequenceEqual(self.rock.members.all(), [self.bob]) self.assertEqual(self.rock.membership_set.get().invite_reason, 'He is good.') def test_add_on_m2m_with_intermediate_model_callable_through_default(self): def invite_reason_callable(): return 'They were good at %s' % datetime.now() self.rock.members.add( self.bob, self.jane, through_defaults={'invite_reason': invite_reason_callable}, ) self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane]) self.assertEqual( self.rock.membership_set.filter( invite_reason__startswith='They were good at ', ).count(), 2, ) # invite_reason_callable() is called once. self.assertEqual( self.bob.membership_set.get().invite_reason, self.jane.membership_set.get().invite_reason, ) def test_set_on_m2m_with_intermediate_model_callable_through_default(self): self.rock.members.set( [self.bob, self.jane], through_defaults={'invite_reason': lambda: 'Why not?'}, ) self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane]) self.assertEqual( self.rock.membership_set.filter( invite_reason__startswith='Why not?', ).count(), 2, ) def test_add_on_m2m_with_intermediate_model_value_required(self): self.rock.nodefaultsnonulls.add(self.jim, through_defaults={'nodefaultnonull': 1}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) def test_add_on_m2m_with_intermediate_model_value_required_fails(self): with self.assertRaises(IntegrityError): self.rock.nodefaultsnonulls.add(self.jim) def test_create_on_m2m_with_intermediate_model(self): annie = self.rock.members.create(name='Annie', through_defaults={'invite_reason': 'She was just awesome.'}) self.assertSequenceEqual(self.rock.members.all(), [annie]) self.assertEqual(self.rock.membership_set.get().invite_reason, 'She was just awesome.') def test_create_on_m2m_with_intermediate_model_callable_through_default(self): annie = self.rock.members.create( name='Annie', through_defaults={'invite_reason': lambda: 'She was just awesome.'}, ) self.assertSequenceEqual(self.rock.members.all(), [annie]) self.assertEqual( self.rock.membership_set.get().invite_reason, 'She was just awesome.', ) def test_create_on_m2m_with_intermediate_model_value_required(self): self.rock.nodefaultsnonulls.create(name='Test', through_defaults={'nodefaultnonull': 1}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) def test_create_on_m2m_with_intermediate_model_value_required_fails(self): with self.assertRaises(IntegrityError): self.rock.nodefaultsnonulls.create(name='Test') def test_get_or_create_on_m2m_with_intermediate_model_value_required(self): self.rock.nodefaultsnonulls.get_or_create(name='Test', through_defaults={'nodefaultnonull': 1}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) def test_get_or_create_on_m2m_with_intermediate_model_value_required_fails(self): with self.assertRaises(IntegrityError): self.rock.nodefaultsnonulls.get_or_create(name='Test') def test_update_or_create_on_m2m_with_intermediate_model_value_required(self): self.rock.nodefaultsnonulls.update_or_create(name='Test', through_defaults={'nodefaultnonull': 1}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) def test_update_or_create_on_m2m_with_intermediate_model_value_required_fails(self): with self.assertRaises(IntegrityError): self.rock.nodefaultsnonulls.update_or_create(name='Test') def test_remove_on_m2m_with_intermediate_model(self): Membership.objects.create(person=self.jim, group=self.rock) self.rock.members.remove(self.jim) self.assertSequenceEqual(self.rock.members.all(), []) def test_remove_on_m2m_with_intermediate_model_multiple(self): Membership.objects.create(person=self.jim, group=self.rock, invite_reason='1') Membership.objects.create(person=self.jim, group=self.rock, invite_reason='2') self.assertSequenceEqual(self.rock.members.all(), [self.jim, self.jim]) self.rock.members.remove(self.jim) self.assertSequenceEqual(self.rock.members.all(), []) def test_set_on_m2m_with_intermediate_model(self): members = list(Person.objects.filter(name__in=['Bob', 'Jim'])) self.rock.members.set(members) self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jim]) def test_set_on_m2m_with_intermediate_model_value_required(self): self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 1}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 2}) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1) self.rock.nodefaultsnonulls.set([self.jim], through_defaults={'nodefaultnonull': 2}, clear=True) self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 2) def test_set_on_m2m_with_intermediate_model_value_required_fails(self): with self.assertRaises(IntegrityError): self.rock.nodefaultsnonulls.set([self.jim]) def test_clear_removes_all_the_m2m_relationships(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jane, group=self.rock) self.rock.members.clear() self.assertQuerysetEqual( self.rock.members.all(), [] ) def test_retrieve_reverse_intermediate_items(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jim, group=self.roll) expected = ['Rock', 'Roll'] self.assertQuerysetEqual( self.jim.group_set.all(), expected, attrgetter("name") ) def test_add_on_reverse_m2m_with_intermediate_model(self): self.bob.group_set.add(self.rock) self.assertSequenceEqual(self.bob.group_set.all(), [self.rock]) def test_create_on_reverse_m2m_with_intermediate_model(self): funk = self.bob.group_set.create(name='Funk') self.assertSequenceEqual(self.bob.group_set.all(), [funk]) def test_remove_on_reverse_m2m_with_intermediate_model(self): Membership.objects.create(person=self.bob, group=self.rock) self.bob.group_set.remove(self.rock) self.assertSequenceEqual(self.bob.group_set.all(), []) def test_set_on_reverse_m2m_with_intermediate_model(self): members = list(Group.objects.filter(name__in=['Rock', 'Roll'])) self.bob.group_set.set(members) self.assertSequenceEqual(self.bob.group_set.all(), [self.rock, self.roll]) def test_clear_on_reverse_removes_all_the_m2m_relationships(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jim, group=self.roll) self.jim.group_set.clear() self.assertQuerysetEqual( self.jim.group_set.all(), [] ) def test_query_model_by_attribute_name_of_related_model(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jane, group=self.rock) Membership.objects.create(person=self.bob, group=self.roll) Membership.objects.create(person=self.jim, group=self.roll) Membership.objects.create(person=self.jane, group=self.roll) self.assertQuerysetEqual( Group.objects.filter(members__name='Bob'), ['Roll'], attrgetter("name") ) def test_order_by_relational_field_through_model(self): today = datetime.now() yesterday = today - timedelta(days=1) CustomMembership.objects.create(person=self.jim, group=self.rock, date_joined=yesterday) CustomMembership.objects.create(person=self.bob, group=self.rock, date_joined=today) CustomMembership.objects.create(person=self.jane, group=self.roll, date_joined=yesterday) CustomMembership.objects.create(person=self.jim, group=self.roll, date_joined=today) self.assertSequenceEqual( self.rock.custom_members.order_by('custom_person_related_name'), [self.jim, self.bob] ) self.assertSequenceEqual( self.roll.custom_members.order_by('custom_person_related_name'), [self.jane, self.jim] ) def test_query_first_model_by_intermediate_model_attribute(self): Membership.objects.create( person=self.jane, group=self.roll, invite_reason="She was just awesome." ) Membership.objects.create( person=self.jim, group=self.roll, invite_reason="He is good." ) Membership.objects.create(person=self.bob, group=self.roll) qs = Group.objects.filter( membership__invite_reason="She was just awesome." ) self.assertQuerysetEqual( qs, ['Roll'], attrgetter("name") ) def test_query_second_model_by_intermediate_model_attribute(self): Membership.objects.create( person=self.jane, group=self.roll, invite_reason="She was just awesome." ) Membership.objects.create( person=self.jim, group=self.roll, invite_reason="He is good." ) Membership.objects.create(person=self.bob, group=self.roll) qs = Person.objects.filter( membership__invite_reason="She was just awesome." ) self.assertQuerysetEqual( qs, ['Jane'], attrgetter("name") ) def test_query_model_by_related_model_name(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create(person=self.jane, group=self.rock) Membership.objects.create(person=self.bob, group=self.roll) Membership.objects.create(person=self.jim, group=self.roll) Membership.objects.create(person=self.jane, group=self.roll) self.assertQuerysetEqual( Person.objects.filter(group__name="Rock"), ['Jane', 'Jim'], attrgetter("name") ) def test_query_model_by_custom_related_name(self): CustomMembership.objects.create(person=self.bob, group=self.rock) CustomMembership.objects.create(person=self.jim, group=self.rock) self.assertQuerysetEqual( Person.objects.filter(custom__name="Rock"), ['Bob', 'Jim'], attrgetter("name") ) def test_query_model_by_intermediate_can_return_non_unique_queryset(self): Membership.objects.create(person=self.jim, group=self.rock) Membership.objects.create( person=self.jane, group=self.rock, date_joined=datetime(2006, 1, 1) ) Membership.objects.create( person=self.bob, group=self.roll, date_joined=datetime(2004, 1, 1)) Membership.objects.create(person=self.jim, group=self.roll) Membership.objects.create( person=self.jane, group=self.roll, date_joined=datetime(2004, 1, 1)) qs = Person.objects.filter( membership__date_joined__gt=datetime(2004, 1, 1) ) self.assertQuerysetEqual( qs, ['Jane', 'Jim', 'Jim'], attrgetter("name") ) def test_custom_related_name_forward_empty_qs(self): self.assertQuerysetEqual( self.rock.custom_members.all(), [] ) def test_custom_related_name_reverse_empty_qs(self): self.assertQuerysetEqual( self.bob.custom.all(), [] ) def test_custom_related_name_forward_non_empty_qs(self): CustomMembership.objects.create(person=self.bob, group=self.rock) CustomMembership.objects.create(person=self.jim, group=self.rock) self.assertQuerysetEqual( self.rock.custom_members.all(), ['Bob', 'Jim'], attrgetter("name") ) def test_custom_related_name_reverse_non_empty_qs(self): CustomMembership.objects.create(person=self.bob, group=self.rock) CustomMembership.objects.create(person=self.jim, group=self.rock) self.assertQuerysetEqual( self.bob.custom.all(), ['Rock'], attrgetter("name") ) def test_custom_related_name_doesnt_conflict_with_fky_related_name(self): c = CustomMembership.objects.create(person=self.bob, group=self.rock) self.assertSequenceEqual(self.bob.custom_person_related_name.all(), [c]) def test_through_fields(self): """ Relations with intermediary tables with multiple FKs to the M2M's ``to`` model are possible. """ event = Event.objects.create(title='Rockwhale 2014') Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jim) Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jane) self.assertQuerysetEqual( event.invitees.all(), ['Jane', 'Jim'], attrgetter('name') ) class M2mThroughReferentialTests(TestCase): def test_self_referential_empty_qs(self): tony = PersonSelfRefM2M.objects.create(name="Tony") self.assertQuerysetEqual( tony.friends.all(), [] ) def test_self_referential_non_symmetrical_first_side(self): tony = PersonSelfRefM2M.objects.create(name="Tony") chris = PersonSelfRefM2M.objects.create(name="Chris") Friendship.objects.create( first=tony, second=chris, date_friended=datetime.now() ) self.assertQuerysetEqual( tony.friends.all(), ['Chris'], attrgetter("name") ) def test_self_referential_non_symmetrical_second_side(self): tony = PersonSelfRefM2M.objects.create(name="Tony") chris = PersonSelfRefM2M.objects.create(name="Chris") Friendship.objects.create( first=tony, second=chris, date_friended=datetime.now() ) self.assertQuerysetEqual( chris.friends.all(), [] ) def test_self_referential_non_symmetrical_clear_first_side(self): tony = PersonSelfRefM2M.objects.create(name="Tony") chris = PersonSelfRefM2M.objects.create(name="Chris") Friendship.objects.create( first=tony, second=chris, date_friended=datetime.now() ) chris.friends.clear() self.assertQuerysetEqual( chris.friends.all(), [] ) # Since this isn't a symmetrical relation, Tony's friend link still exists. self.assertQuerysetEqual( tony.friends.all(), ['Chris'], attrgetter("name") ) def test_self_referential_non_symmetrical_both(self): tony = PersonSelfRefM2M.objects.create(name="Tony") chris = PersonSelfRefM2M.objects.create(name="Chris") Friendship.objects.create( first=tony, second=chris, date_friended=datetime.now() ) Friendship.objects.create( first=chris, second=tony, date_friended=datetime.now() ) self.assertQuerysetEqual( tony.friends.all(), ['Chris'], attrgetter("name") ) self.assertQuerysetEqual( chris.friends.all(), ['Tony'], attrgetter("name") ) def test_through_fields_self_referential(self): john = Employee.objects.create(name='john') peter = Employee.objects.create(name='peter') mary = Employee.objects.create(name='mary') harry = Employee.objects.create(name='harry') Relationship.objects.create(source=john, target=peter, another=None) Relationship.objects.create(source=john, target=mary, another=None) Relationship.objects.create(source=john, target=harry, another=peter) self.assertQuerysetEqual( john.subordinates.all(), ['peter', 'mary', 'harry'], attrgetter('name') ) def test_self_referential_symmetrical(self): tony = PersonSelfRefM2M.objects.create(name='Tony') chris = PersonSelfRefM2M.objects.create(name='Chris') SymmetricalFriendship.objects.create( first=tony, second=chris, date_friended=date.today(), ) self.assertSequenceEqual(tony.sym_friends.all(), [chris]) # Manually created symmetrical m2m relation doesn't add mirror entry # automatically. self.assertSequenceEqual(chris.sym_friends.all(), []) SymmetricalFriendship.objects.create( first=chris, second=tony, date_friended=date.today() ) self.assertSequenceEqual(chris.sym_friends.all(), [tony]) def test_add_on_symmetrical_m2m_with_intermediate_model(self): tony = PersonSelfRefM2M.objects.create(name='Tony') chris = PersonSelfRefM2M.objects.create(name='Chris') date_friended = date(2017, 1, 3) tony.sym_friends.add(chris, through_defaults={'date_friended': date_friended}) self.assertSequenceEqual(tony.sym_friends.all(), [chris]) self.assertSequenceEqual(chris.sym_friends.all(), [tony]) friendship = tony.symmetricalfriendship_set.get() self.assertEqual(friendship.date_friended, date_friended) def test_set_on_symmetrical_m2m_with_intermediate_model(self): tony = PersonSelfRefM2M.objects.create(name='Tony') chris = PersonSelfRefM2M.objects.create(name='Chris') anne = PersonSelfRefM2M.objects.create(name='Anne') kate = PersonSelfRefM2M.objects.create(name='Kate') date_friended_add = date(2013, 1, 5) date_friended_set = date.today() tony.sym_friends.add( anne, chris, through_defaults={'date_friended': date_friended_add}, ) tony.sym_friends.set( [anne, kate], through_defaults={'date_friended': date_friended_set}, ) self.assertSequenceEqual(tony.sym_friends.all(), [anne, kate]) self.assertSequenceEqual(anne.sym_friends.all(), [tony]) self.assertSequenceEqual(kate.sym_friends.all(), [tony]) self.assertEqual( kate.symmetricalfriendship_set.get().date_friended, date_friended_set, ) # Date is preserved. self.assertEqual( anne.symmetricalfriendship_set.get().date_friended, date_friended_add, ) # Recreate relationship. tony.sym_friends.set( [anne], clear=True, through_defaults={'date_friended': date_friended_set}, ) self.assertSequenceEqual(tony.sym_friends.all(), [anne]) self.assertSequenceEqual(anne.sym_friends.all(), [tony]) self.assertEqual( anne.symmetricalfriendship_set.get().date_friended, date_friended_set, ) class M2mThroughToFieldsTests(TestCase): @classmethod def setUpTestData(cls): cls.pea = Ingredient.objects.create(iname='pea') cls.potato = Ingredient.objects.create(iname='potato') cls.tomato = Ingredient.objects.create(iname='tomato') cls.curry = Recipe.objects.create(rname='curry') RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.potato) RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.pea) RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.tomato) def test_retrieval(self): # Forward retrieval self.assertSequenceEqual(self.curry.ingredients.all(), [self.pea, self.potato, self.tomato]) # Backward retrieval self.assertEqual(self.tomato.recipes.get(), self.curry) def test_choices(self): field = Recipe._meta.get_field('ingredients') self.assertEqual( [choice[0] for choice in field.get_choices(include_blank=False)], ['pea', 'potato', 'tomato'] )