Skip to content

Commit

Permalink
[1.11.x] Fixed #28222 -- Allowed settable properties in QuerySet.upda…
Browse files Browse the repository at this point in the history
…te_or_create()/get_or_create() defaults.

Backport of 37ab3c3 from master
  • Loading branch information
365g1s22 authored and timgraham committed May 27, 2017
1 parent f804b46 commit b9abdd9
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 5 deletions.
5 changes: 1 addition & 4 deletions django/db/models/options.py
Expand Up @@ -881,10 +881,7 @@ def has_auto_field(self, value):

@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)
Expand Down
4 changes: 3 additions & 1 deletion django/db/models/query.py
Expand Up @@ -518,12 +518,14 @@ def _extract_model_params(self, defaults, **kwargs):
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(
Expand Down
4 changes: 4 additions & 0 deletions docs/releases/1.11.2.txt
Expand Up @@ -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`).
12 changes: 12 additions & 0 deletions tests/get_or_create/models.py
Expand Up @@ -36,6 +36,18 @@ class Thing(models.Model):
name = models.CharField(max_length=256)
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)
Expand Down
18 changes: 18 additions & 0 deletions tests/get_or_create/tests.py
Expand Up @@ -77,6 +77,11 @@ def test_get_or_create_with_pk_property(self):
"""
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.
Expand Down Expand Up @@ -336,6 +341,11 @@ def test_with_pk_property(self):
"""
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.
Expand Down Expand Up @@ -522,3 +532,11 @@ def test_update_or_create_with_invalid_kwargs(self):
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'})

0 comments on commit b9abdd9

Please sign in to comment.