diff --git a/django/db/models/fields/reverse_related.py b/django/db/models/fields/reverse_related.py index 77c5d23d69..57f1c4d389 100644 --- a/django/db/models/fields/reverse_related.py +++ b/django/db/models/fields/reverse_related.py @@ -11,6 +11,7 @@ they're the closest concept currently available. from django.core import exceptions from django.utils.functional import cached_property +from django.utils.hashable import make_hashable from . import BLANK_CHOICE_DASH from .mixins import FieldCacheMixin @@ -115,6 +116,28 @@ class ForeignObjectRel(FieldCacheMixin): self.related_model._meta.model_name, ) + @property + def identity(self): + return ( + self.field, + self.model, + self.related_name, + self.related_query_name, + tuple(sorted(make_hashable(self.limit_choices_to))), + self.parent_link, + self.on_delete, + self.symmetrical, + self.multiple, + ) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self.identity == other.identity + + def __hash__(self): + return hash(self.identity) + def get_choices( self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_choices_to=None, ordering=(), @@ -215,6 +238,10 @@ class ManyToOneRel(ForeignObjectRel): state.pop('related_model', None) return state + @property + def identity(self): + return super().identity + (self.field_name,) + def get_related_field(self): """ Return the Field in the 'to' object to which this relationship is tied. @@ -279,6 +306,14 @@ class ManyToManyRel(ForeignObjectRel): self.symmetrical = symmetrical self.db_constraint = db_constraint + @property + def identity(self): + return super().identity + ( + self.through, + self.through_fields, + self.db_constraint, + ) + def get_related_field(self): """ Return the field in the 'to' object to which this relationship is tied. diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index 8575c6fe81..76a5c332c7 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -219,6 +219,40 @@ class PickleabilityTestCase(TestCase): with self.assertNumQueries(0): self.assert_pickles(groups) + def test_pickle_filteredrelation(self): + group = Group.objects.create(name='group') + event_1 = Event.objects.create(title='Big event', group=group) + event_2 = Event.objects.create(title='Small event', group=group) + Happening.objects.bulk_create([ + Happening(event=event_1, number1=5), + Happening(event=event_2, number1=3), + ]) + groups = Group.objects.annotate( + big_events=models.FilteredRelation( + 'event', + condition=models.Q(event__title__startswith='Big'), + ), + ).annotate(sum_number=models.Sum('big_events__happening__number1')) + groups_query = pickle.loads(pickle.dumps(groups.query)) + groups = Group.objects.all() + groups.query = groups_query + self.assertEqual(groups.get().sum_number, 5) + + def test_pickle_filteredrelation_m2m(self): + group = Group.objects.create(name='group') + m2mmodel = M2MModel.objects.create() + m2mmodel.groups.add(group) + groups = Group.objects.annotate( + first_m2mmodels=models.FilteredRelation( + 'm2mmodel', + condition=models.Q(m2mmodel__pk__lt=10), + ), + ).annotate(count_groups=models.Count('first_m2mmodels__groups')) + groups_query = pickle.loads(pickle.dumps(groups.query)) + groups = Group.objects.all() + groups.query = groups_query + self.assertEqual(groups.get().count_groups, 1) + def test_annotation_with_callable_default(self): # Happening.when has a callable default of datetime.datetime.now. qs = Happening.objects.annotate(latest_time=models.Max('when'))