diff --git a/django/db/models/query.py b/django/db/models/query.py index fddcbb5bbe..dac6b1c1c8 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -527,6 +527,18 @@ class QuerySet(object): 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) + invalid_params = [] + for param in params: + try: + self.model._meta.get_field(param) + except exceptions.FieldDoesNotExist: + invalid_params.append(param) + if invalid_params: + raise exceptions.FieldError( + "Invalid field name(s) for model %s: '%s'." % ( + self.model._meta.object_name, + "', '".join(sorted(invalid_params)), + )) return lookup, params def _earliest_or_latest(self, field_name=None, direction="-"): diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index 73240c76ac..ded12408b9 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -446,6 +446,15 @@ Protection against insecure redirects in :mod:`django.contrib.auth` and ``i18n`` and :func:`~django.views.i18n.set_language` protect users from being redirected to non-HTTPS ``next`` URLs when the app is running over HTTPS. +``QuerySet.get_or_create()`` and ``update_or_create()`` validate arguments +-------------------------------------------------------------------------- + +To prevent typos from passing silently, +:meth:`~django.db.models.query.QuerySet.get_or_create` and +:meth:`~django.db.models.query.QuerySet.update_or_create` check that their +arguments are model fields. This should be backwards-incompatible only in the +fact that it might expose a bug in your project. + Miscellaneous ------------- diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 108c8e3b4f..c569d938b0 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -5,9 +5,10 @@ import traceback from datetime import date, datetime, timedelta from threading import Thread +from django.core.exceptions import FieldError from django.db import DatabaseError, IntegrityError, connection from django.test import ( - TestCase, TransactionTestCase, ignore_warnings, skipUnlessDBFeature, + SimpleTestCase, TestCase, TransactionTestCase, ignore_warnings, skipUnlessDBFeature, ) from django.utils.encoding import DjangoUnicodeDecodeError @@ -484,3 +485,27 @@ class UpdateOrCreateTransactionTests(TransactionTestCase): updated_person = Person.objects.get(first_name='John') self.assertGreater(after_update - before_start, timedelta(seconds=0.5)) self.assertEqual(updated_person.last_name, 'NotLennon') + + +class InvalidCreateArgumentsTests(SimpleTestCase): + msg = "Invalid field name(s) for model Thing: 'nonexistent'." + + def test_get_or_create_with_invalid_defaults(self): + with self.assertRaisesMessage(FieldError, self.msg): + Thing.objects.get_or_create(name='a', defaults={'nonexistent': 'b'}) + + def test_get_or_create_with_invalid_kwargs(self): + with self.assertRaisesMessage(FieldError, self.msg): + Thing.objects.get_or_create(name='a', nonexistent='b') + + def test_update_or_create_with_invalid_defaults(self): + with self.assertRaisesMessage(FieldError, self.msg): + Thing.objects.update_or_create(name='a', defaults={'nonexistent': 'b'}) + + def test_update_or_create_with_invalid_kwargs(self): + with self.assertRaisesMessage(FieldError, self.msg): + Thing.objects.update_or_create(name='a', nonexistent='b') + + 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'})