summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex <alex@gmail.com>2017-05-19 06:40:43 -0400
committerTim Graham <timograham@gmail.com>2017-05-27 12:41:38 -0400
commit37ab3c3f9d707d6a1896db79c631e920dcb1fb78 (patch)
treee373ed7275a992675ef97ccf987ae2fc78b3b17e
parent385cf7091e066604f9e417c6ef63685492a08d13 (diff)
Fixed #28222 -- Allowed settable properties in QuerySet.update_or_create()/get_or_create() defaults.
-rw-r--r--django/db/models/options.py5
-rw-r--r--django/db/models/query.py4
-rw-r--r--docs/releases/1.11.2.txt4
-rw-r--r--tests/get_or_create/models.py12
-rw-r--r--tests/get_or_create/tests.py18
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'})