[1.11.x] Fixed #28222 -- Allowed settable properties in QuerySet.update_or_create()/get_or_create() defaults.
Backport of 37ab3c3f9d
from master
This commit is contained in:
parent
f804b46294
commit
b9abdd92ab
|
@ -881,10 +881,7 @@ class Options(object):
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def _property_names(self):
|
def _property_names(self):
|
||||||
"""
|
"""Return a set of the names of the properties defined on the model."""
|
||||||
Return a set of the names of the properties defined on the model.
|
|
||||||
Internal helper for model initialization.
|
|
||||||
"""
|
|
||||||
return frozenset({
|
return frozenset({
|
||||||
attr for attr in
|
attr for attr in
|
||||||
dir(self.model) if isinstance(getattr(self.model, attr), property)
|
dir(self.model) if isinstance(getattr(self.model, attr), property)
|
||||||
|
|
|
@ -518,12 +518,14 @@ class QuerySet(object):
|
||||||
lookup[f.name] = lookup.pop(f.attname)
|
lookup[f.name] = lookup.pop(f.attname)
|
||||||
params = {k: v for k, v in kwargs.items() if LOOKUP_SEP not in k}
|
params = {k: v for k, v in kwargs.items() if LOOKUP_SEP not in k}
|
||||||
params.update(defaults)
|
params.update(defaults)
|
||||||
|
property_names = self.model._meta._property_names
|
||||||
invalid_params = []
|
invalid_params = []
|
||||||
for param in params:
|
for param in params:
|
||||||
try:
|
try:
|
||||||
self.model._meta.get_field(param)
|
self.model._meta.get_field(param)
|
||||||
except exceptions.FieldDoesNotExist:
|
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)
|
invalid_params.append(param)
|
||||||
if invalid_params:
|
if invalid_params:
|
||||||
raise exceptions.FieldError(
|
raise exceptions.FieldError(
|
||||||
|
|
|
@ -32,3 +32,7 @@ Bugfixes
|
||||||
|
|
||||||
* Allowed ``DjangoJSONEncoder`` to serialize
|
* Allowed ``DjangoJSONEncoder`` to serialize
|
||||||
``django.utils.deprecation.CallableBool`` (:ticket:`28230`).
|
``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`).
|
||||||
|
|
|
@ -36,6 +36,18 @@ class Thing(models.Model):
|
||||||
name = models.CharField(max_length=256)
|
name = models.CharField(max_length=256)
|
||||||
tags = models.ManyToManyField(Tag)
|
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):
|
class Publisher(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
|
|
@ -77,6 +77,11 @@ class GetOrCreateTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Thing.objects.get_or_create(pk=1)
|
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):
|
def test_get_or_create_on_related_manager(self):
|
||||||
p = Publisher.objects.create(name="Acme Publishing")
|
p = Publisher.objects.create(name="Acme Publishing")
|
||||||
# Create a book through the publisher.
|
# Create a book through the publisher.
|
||||||
|
@ -336,6 +341,11 @@ class UpdateOrCreateTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Thing.objects.update_or_create(pk=1)
|
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):
|
def test_error_contains_full_traceback(self):
|
||||||
"""
|
"""
|
||||||
update_or_create should raise IntegrityErrors with the full traceback.
|
update_or_create should raise IntegrityErrors with the full traceback.
|
||||||
|
@ -522,3 +532,11 @@ class InvalidCreateArgumentsTests(SimpleTestCase):
|
||||||
def test_multiple_invalid_fields(self):
|
def test_multiple_invalid_fields(self):
|
||||||
with self.assertRaisesMessage(FieldError, "Invalid field name(s) for model Thing: 'invalid', 'nonexistent'"):
|
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'})
|
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'})
|
||||||
|
|
Loading…
Reference in New Issue