From 37ab3c3f9d707d6a1896db79c631e920dcb1fb78 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 19 May 2017 06:40:43 -0400 Subject: [PATCH] Fixed #28222 -- Allowed settable properties in QuerySet.update_or_create()/get_or_create() defaults. --- django/db/models/options.py | 5 +---- django/db/models/query.py | 4 +++- docs/releases/1.11.2.txt | 4 ++++ tests/get_or_create/models.py | 12 ++++++++++++ tests/get_or_create/tests.py | 18 ++++++++++++++++++ 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index cd50ff0aaa..557455c128 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -828,10 +828,7 @@ class Options: @cached_property def _property_names(self): - """ - Return a set of the names of the properties defined on the model. - Internal helper for model initialization. - """ + """Return a set of the names of the properties defined on the model.""" return frozenset({ attr for attr in dir(self.model) if isinstance(getattr(self.model, attr), property) diff --git a/django/db/models/query.py b/django/db/models/query.py index 9fd116c4a6..10952195db 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -504,12 +504,14 @@ class QuerySet: lookup[f.name] = lookup.pop(f.attname) params = {k: v for k, v in kwargs.items() if LOOKUP_SEP not in k} params.update(defaults) + property_names = self.model._meta._property_names invalid_params = [] for param in params: try: self.model._meta.get_field(param) except exceptions.FieldDoesNotExist: - if param != 'pk': # It's okay to use a model's pk property. + # It's okay to use a model's property if it has a setter. + if not (param in property_names and getattr(self.model, param).fset): invalid_params.append(param) if invalid_params: raise exceptions.FieldError( diff --git a/docs/releases/1.11.2.txt b/docs/releases/1.11.2.txt index e1cd939fa0..ef235fa9ed 100644 --- a/docs/releases/1.11.2.txt +++ b/docs/releases/1.11.2.txt @@ -32,3 +32,7 @@ Bugfixes * Allowed ``DjangoJSONEncoder`` to serialize ``django.utils.deprecation.CallableBool`` (:ticket:`28230`). + +* Relaxed the validation added in Django 1.11 of the fields in the ``defaults`` + argument of ``QuerySet.get_or_create()`` and ``update_or_create()`` to + reallow settable model properties (:ticket:`28222`). diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 865798ae98..4a33a809bb 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -32,6 +32,18 @@ class Thing(models.Model): name = models.CharField(max_length=255) tags = models.ManyToManyField(Tag) + @property + def capitalized_name_property(self): + return self.name + + @capitalized_name_property.setter + def capitalized_name_property(self, val): + self.name = val.capitalize() + + @property + def name_in_all_caps(self): + return self.name.upper() + class Publisher(models.Model): name = models.CharField(max_length=100) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 2ac5a862b0..60e1ef23f2 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -73,6 +73,11 @@ class GetOrCreateTests(TestCase): """ Thing.objects.get_or_create(pk=1) + def test_get_or_create_with_model_property_defaults(self): + """Using a property with a setter implemented is allowed.""" + t, _ = Thing.objects.get_or_create(defaults={'capitalized_name_property': 'annie'}, pk=1) + self.assertEqual(t.name, 'Annie') + def test_get_or_create_on_related_manager(self): p = Publisher.objects.create(name="Acme Publishing") # Create a book through the publisher. @@ -328,6 +333,11 @@ class UpdateOrCreateTests(TestCase): """ Thing.objects.update_or_create(pk=1) + def test_update_or_create_with_model_property_defaults(self): + """Using a property with a setter implemented is allowed.""" + t, _ = Thing.objects.get_or_create(defaults={'capitalized_name_property': 'annie'}, pk=1) + self.assertEqual(t.name, 'Annie') + def test_error_contains_full_traceback(self): """ update_or_create should raise IntegrityErrors with the full traceback. @@ -514,3 +524,11 @@ class InvalidCreateArgumentsTests(SimpleTestCase): def test_multiple_invalid_fields(self): with self.assertRaisesMessage(FieldError, "Invalid field name(s) for model Thing: 'invalid', 'nonexistent'"): Thing.objects.update_or_create(name='a', nonexistent='b', defaults={'invalid': 'c'}) + + def test_property_attribute_without_setter_defaults(self): + with self.assertRaisesMessage(FieldError, "Invalid field name(s) for model Thing: 'name_in_all_caps'"): + Thing.objects.update_or_create(name='a', defaults={'name_in_all_caps': 'FRANK'}) + + def test_property_attribute_without_setter_kwargs(self): + with self.assertRaisesMessage(FieldError, "Invalid field name(s) for model Thing: 'name_in_all_caps'"): + Thing.objects.update_or_create(name_in_all_caps='FRANK', defaults={'name': 'Frank'})