From 89f40e36246100df6a11316c31a76712ebc6c501 Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 26 Feb 2013 09:53:47 +0100 Subject: Merged regressiontests and modeltests into the test root. --- tests/basic/__init__.py | 0 tests/basic/models.py | 20 ++ tests/basic/tests.py | 693 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 713 insertions(+) create mode 100644 tests/basic/__init__.py create mode 100644 tests/basic/models.py create mode 100644 tests/basic/tests.py (limited to 'tests/basic') diff --git a/tests/basic/__init__.py b/tests/basic/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/basic/models.py b/tests/basic/models.py new file mode 100644 index 0000000000..660beddf49 --- /dev/null +++ b/tests/basic/models.py @@ -0,0 +1,20 @@ +# coding: utf-8 +""" +1. Bare-bones model + +This is a basic model with only two non-primary-key fields. +""" +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +@python_2_unicode_compatible +class Article(models.Model): + headline = models.CharField(max_length=100, default='Default headline') + pub_date = models.DateTimeField() + + class Meta: + ordering = ('pub_date','headline') + + def __str__(self): + return self.headline diff --git a/tests/basic/tests.py b/tests/basic/tests.py new file mode 100644 index 0000000000..2de87a225f --- /dev/null +++ b/tests/basic/tests.py @@ -0,0 +1,693 @@ +from __future__ import absolute_import, unicode_literals + +from datetime import datetime + +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError +from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.utils import six +from django.utils.translation import ugettext_lazy + +from .models import Article + + +class ModelTest(TestCase): + + def test_lookup(self): + # No articles are in the system yet. + self.assertQuerysetEqual(Article.objects.all(), []) + + # Create an Article. + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + + # Save it into the database. You have to call save() explicitly. + a.save() + + # Now it has an ID. + self.assertTrue(a.id != None) + + # Models have a pk property that is an alias for the primary key + # attribute (by default, the 'id' attribute). + self.assertEqual(a.pk, a.id) + + # Access database columns via Python attributes. + self.assertEqual(a.headline, 'Area man programs in Python') + self.assertEqual(a.pub_date, datetime(2005, 7, 28, 0, 0)) + + # Change values by changing the attributes, then calling save(). + a.headline = 'Area woman programs in Python' + a.save() + + # Article.objects.all() returns all the articles in the database. + self.assertQuerysetEqual(Article.objects.all(), + ['']) + + # Django provides a rich database lookup API. + self.assertEqual(Article.objects.get(id__exact=a.id), a) + self.assertEqual(Article.objects.get(headline__startswith='Area woman'), a) + self.assertEqual(Article.objects.get(pub_date__year=2005), a) + self.assertEqual(Article.objects.get(pub_date__year=2005, pub_date__month=7), a) + self.assertEqual(Article.objects.get(pub_date__year=2005, pub_date__month=7, pub_date__day=28), a) + self.assertEqual(Article.objects.get(pub_date__week_day=5), a) + + # The "__exact" lookup type can be omitted, as a shortcut. + self.assertEqual(Article.objects.get(id=a.id), a) + self.assertEqual(Article.objects.get(headline='Area woman programs in Python'), a) + + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2005), + [''], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2004), + [], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__year=2005, pub_date__month=7), + [''], + ) + + self.assertQuerysetEqual( + Article.objects.filter(pub_date__week_day=5), + [''], + ) + self.assertQuerysetEqual( + Article.objects.filter(pub_date__week_day=6), + [], + ) + + # Django raises an Article.DoesNotExist exception for get() if the + # parameters don't match any object. + six.assertRaisesRegex(self, + ObjectDoesNotExist, + "Article matching query does not exist. Lookup parameters were " + "{'id__exact': 2000}", + Article.objects.get, + id__exact=2000, + ) + # To avoid dict-ordering related errors check only one lookup + # in single assert. + six.assertRaisesRegex(self, + ObjectDoesNotExist, + ".*'pub_date__year': 2005.*", + Article.objects.get, + pub_date__year=2005, + pub_date__month=8, + ) + six.assertRaisesRegex(self, + ObjectDoesNotExist, + ".*'pub_date__month': 8.*", + Article.objects.get, + pub_date__year=2005, + pub_date__month=8, + ) + + six.assertRaisesRegex(self, + ObjectDoesNotExist, + "Article matching query does not exist. Lookup parameters were " + "{'pub_date__week_day': 6}", + Article.objects.get, + pub_date__week_day=6, + ) + + # Lookup by a primary key is the most common case, so Django + # provides a shortcut for primary-key exact lookups. + # The following is identical to articles.get(id=a.id). + self.assertEqual(Article.objects.get(pk=a.id), a) + + # pk can be used as a shortcut for the primary key name in any query. + self.assertQuerysetEqual(Article.objects.filter(pk__in=[a.id]), + [""]) + + # Model instances of the same type and same ID are considered equal. + a = Article.objects.get(pk=a.id) + b = Article.objects.get(pk=a.id) + self.assertEqual(a, b) + + # Create a very similar object + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + a.save() + + self.assertEqual(Article.objects.count(), 2) + + # Django raises an Article.MultipleObjectsReturned exception if the + # lookup matches more than one object + six.assertRaisesRegex(self, + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + headline__startswith='Area', + ) + + six.assertRaisesRegex(self, + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + pub_date__year=2005, + ) + + six.assertRaisesRegex(self, + MultipleObjectsReturned, + "get\(\) returned more than one Article -- it returned 2!", + Article.objects.get, + pub_date__year=2005, + pub_date__month=7, + ) + + def test_object_creation(self): + # Create an Article. + a = Article( + id=None, + headline='Area man programs in Python', + pub_date=datetime(2005, 7, 28), + ) + + # Save it into the database. You have to call save() explicitly. + a.save() + + # You can initialize a model instance using positional arguments, + # which should match the field order as defined in the model. + a2 = Article(None, 'Second article', datetime(2005, 7, 29)) + a2.save() + + self.assertNotEqual(a2.id, a.id) + self.assertEqual(a2.headline, 'Second article') + self.assertEqual(a2.pub_date, datetime(2005, 7, 29, 0, 0)) + + # ...or, you can use keyword arguments. + a3 = Article( + id=None, + headline='Third article', + pub_date=datetime(2005, 7, 30), + ) + a3.save() + + self.assertNotEqual(a3.id, a.id) + self.assertNotEqual(a3.id, a2.id) + self.assertEqual(a3.headline, 'Third article') + self.assertEqual(a3.pub_date, datetime(2005, 7, 30, 0, 0)) + + # You can also mix and match position and keyword arguments, but + # be sure not to duplicate field information. + a4 = Article(None, 'Fourth article', pub_date=datetime(2005, 7, 31)) + a4.save() + self.assertEqual(a4.headline, 'Fourth article') + + # Don't use invalid keyword arguments. + six.assertRaisesRegex(self, + TypeError, + "'foo' is an invalid keyword argument for this function", + Article, + id=None, + headline='Invalid', + pub_date=datetime(2005, 7, 31), + foo='bar', + ) + + # You can leave off the value for an AutoField when creating an + # object, because it'll get filled in automatically when you save(). + a5 = Article(headline='Article 6', pub_date=datetime(2005, 7, 31)) + a5.save() + self.assertEqual(a5.headline, 'Article 6') + + # If you leave off a field with "default" set, Django will use + # the default. + a6 = Article(pub_date=datetime(2005, 7, 31)) + a6.save() + self.assertEqual(a6.headline, 'Default headline') + + # For DateTimeFields, Django saves as much precision (in seconds) + # as you give it. + a7 = Article( + headline='Article 7', + pub_date=datetime(2005, 7, 31, 12, 30), + ) + a7.save() + self.assertEqual(Article.objects.get(id__exact=a7.id).pub_date, + datetime(2005, 7, 31, 12, 30)) + + a8 = Article( + headline='Article 8', + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a8.save() + self.assertEqual(Article.objects.get(id__exact=a8.id).pub_date, + datetime(2005, 7, 31, 12, 30, 45)) + + # Saving an object again doesn't create a new object -- it just saves + # the old one. + current_id = a8.id + a8.save() + self.assertEqual(a8.id, current_id) + a8.headline = 'Updated article 8' + a8.save() + self.assertEqual(a8.id, current_id) + + # Check that != and == operators behave as expecte on instances + self.assertTrue(a7 != a8) + self.assertFalse(a7 == a8) + self.assertEqual(a8, Article.objects.get(id__exact=a8.id)) + + self.assertTrue(Article.objects.get(id__exact=a8.id) != Article.objects.get(id__exact=a7.id)) + self.assertFalse(Article.objects.get(id__exact=a8.id) == Article.objects.get(id__exact=a7.id)) + + # You can use 'in' to test for membership... + self.assertTrue(a8 in Article.objects.all()) + + # ... but there will often be more efficient ways if that is all you need: + self.assertTrue(Article.objects.filter(id=a8.id).exists()) + + # datetimes() returns a list of available dates of the given scope for + # the given field. + self.assertQuerysetEqual( + Article.objects.datetimes('pub_date', 'year'), + ["datetime.datetime(2005, 1, 1, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.datetimes('pub_date', 'month'), + ["datetime.datetime(2005, 7, 1, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.datetimes('pub_date', 'day'), + ["datetime.datetime(2005, 7, 28, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 31, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.datetimes('pub_date', 'day', order='ASC'), + ["datetime.datetime(2005, 7, 28, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 31, 0, 0)"]) + self.assertQuerysetEqual( + Article.objects.datetimes('pub_date', 'day', order='DESC'), + ["datetime.datetime(2005, 7, 31, 0, 0)", + "datetime.datetime(2005, 7, 30, 0, 0)", + "datetime.datetime(2005, 7, 29, 0, 0)", + "datetime.datetime(2005, 7, 28, 0, 0)"]) + + # datetimes() requires valid arguments. + self.assertRaises( + TypeError, + Article.objects.dates, + ) + + six.assertRaisesRegex(self, + FieldDoesNotExist, + "Article has no field named 'invalid_field'", + Article.objects.dates, + "invalid_field", + "year", + ) + + six.assertRaisesRegex(self, + AssertionError, + "'kind' must be one of 'year', 'month' or 'day'.", + Article.objects.dates, + "pub_date", + "bad_kind", + ) + + six.assertRaisesRegex(self, + AssertionError, + "'order' must be either 'ASC' or 'DESC'.", + Article.objects.dates, + "pub_date", + "year", + order="bad order", + ) + + # Use iterator() with datetimes() to return a generator that lazily + # requests each result one at a time, to save memory. + dates = [] + for article in Article.objects.datetimes('pub_date', 'day', order='DESC').iterator(): + dates.append(article) + self.assertEqual(dates, [ + datetime(2005, 7, 31, 0, 0), + datetime(2005, 7, 30, 0, 0), + datetime(2005, 7, 29, 0, 0), + datetime(2005, 7, 28, 0, 0)]) + + # You can combine queries with & and |. + s1 = Article.objects.filter(id__exact=a.id) + s2 = Article.objects.filter(id__exact=a2.id) + self.assertQuerysetEqual(s1 | s2, + ["", + ""]) + self.assertQuerysetEqual(s1 & s2, []) + + # You can get the number of objects like this: + self.assertEqual(len(Article.objects.filter(id__exact=a.id)), 1) + + # You can get items using index and slice notation. + self.assertEqual(Article.objects.all()[0], a) + self.assertQuerysetEqual(Article.objects.all()[1:3], + ["", ""]) + + s3 = Article.objects.filter(id__exact=a3.id) + self.assertQuerysetEqual((s1 | s2 | s3)[::2], + ["", + ""]) + + # Slicing works with longs (Python 2 only -- Python 3 doesn't have longs). + if not six.PY3: + self.assertEqual(Article.objects.all()[long(0)], a) + self.assertQuerysetEqual(Article.objects.all()[long(1):long(3)], + ["", ""]) + self.assertQuerysetEqual((s1 | s2 | s3)[::long(2)], + ["", + ""]) + + # And can be mixed with ints. + self.assertQuerysetEqual(Article.objects.all()[1:long(3)], + ["", ""]) + + # Slices (without step) are lazy: + self.assertQuerysetEqual(Article.objects.all()[0:5].filter(), + ["", + "", + "", + "", + ""]) + + # Slicing again works: + self.assertQuerysetEqual(Article.objects.all()[0:5][0:2], + ["", + ""]) + self.assertQuerysetEqual(Article.objects.all()[0:5][:2], + ["", + ""]) + self.assertQuerysetEqual(Article.objects.all()[0:5][4:], + [""]) + self.assertQuerysetEqual(Article.objects.all()[0:5][5:], []) + + # Some more tests! + self.assertQuerysetEqual(Article.objects.all()[2:][0:2], + ["", ""]) + self.assertQuerysetEqual(Article.objects.all()[2:][:2], + ["", ""]) + self.assertQuerysetEqual(Article.objects.all()[2:][2:3], + [""]) + + # Using an offset without a limit is also possible. + self.assertQuerysetEqual(Article.objects.all()[5:], + ["", + "", + ""]) + + # Also, once you have sliced you can't filter, re-order or combine + six.assertRaisesRegex(self, + AssertionError, + "Cannot filter a query once a slice has been taken.", + Article.objects.all()[0:5].filter, + id=a.id, + ) + + six.assertRaisesRegex(self, + AssertionError, + "Cannot reorder a query once a slice has been taken.", + Article.objects.all()[0:5].order_by, + 'id', + ) + + try: + Article.objects.all()[0:1] & Article.objects.all()[4:5] + self.fail('Should raise an AssertionError') + except AssertionError as e: + self.assertEqual(str(e), "Cannot combine queries once a slice has been taken.") + except Exception as e: + self.fail('Should raise an AssertionError, not %s' % e) + + # Negative slices are not supported, due to database constraints. + # (hint: inverting your ordering might do what you need). + try: + Article.objects.all()[-1] + self.fail('Should raise an AssertionError') + except AssertionError as e: + self.assertEqual(str(e), "Negative indexing is not supported.") + except Exception as e: + self.fail('Should raise an AssertionError, not %s' % e) + + error = None + try: + Article.objects.all()[0:-5] + except Exception as e: + error = e + self.assertTrue(isinstance(error, AssertionError)) + self.assertEqual(str(error), "Negative indexing is not supported.") + + # An Article instance doesn't have access to the "objects" attribute. + # That's only available on the class. + six.assertRaisesRegex(self, + AttributeError, + "Manager isn't accessible via Article instances", + getattr, + a7, + "objects", + ) + + # Bulk delete test: How many objects before and after the delete? + self.assertQuerysetEqual(Article.objects.all(), + ["", + "", + "", + "", + "", + "", + "", + ""]) + Article.objects.filter(id__lte=a4.id).delete() + self.assertQuerysetEqual(Article.objects.all(), + ["", + "", + "", + ""]) + + @skipUnlessDBFeature('supports_microsecond_precision') + def test_microsecond_precision(self): + # In PostgreSQL, microsecond-level precision is available. + a9 = Article( + headline='Article 9', + pub_date=datetime(2005, 7, 31, 12, 30, 45, 180), + ) + a9.save() + self.assertEqual(Article.objects.get(pk=a9.pk).pub_date, + datetime(2005, 7, 31, 12, 30, 45, 180)) + + @skipIfDBFeature('supports_microsecond_precision') + def test_microsecond_precision_not_supported(self): + # In MySQL, microsecond-level precision isn't available. You'll lose + # microsecond-level precision once the data is saved. + a9 = Article( + headline='Article 9', + pub_date=datetime(2005, 7, 31, 12, 30, 45, 180), + ) + a9.save() + self.assertEqual(Article.objects.get(id__exact=a9.id).pub_date, + datetime(2005, 7, 31, 12, 30, 45)) + + def test_manually_specify_primary_key(self): + # You can manually specify the primary key when creating a new object. + a101 = Article( + id=101, + headline='Article 101', + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a101.save() + a101 = Article.objects.get(pk=101) + self.assertEqual(a101.headline, 'Article 101') + + def test_create_method(self): + # You can create saved objects in a single step + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + self.assertEqual(Article.objects.get(headline="Article 10"), a10) + + def test_year_lookup_edge_case(self): + # Edge-case test: A year lookup should retrieve all objects in + # the given year, including Jan. 1 and Dec. 31. + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + self.assertQuerysetEqual(Article.objects.filter(pub_date__year=2008), + ["", ""]) + + def test_unicode_data(self): + # Unicode data works, too. + a = Article( + headline='\u6797\u539f \u3081\u3050\u307f', + pub_date=datetime(2005, 7, 28), + ) + a.save() + self.assertEqual(Article.objects.get(pk=a.id).headline, + '\u6797\u539f \u3081\u3050\u307f') + + def test_hash_function(self): + # Model instances have a hash function, so they can be used in sets + # or as dictionary keys. Two models compare as equal if their primary + # keys are equal. + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + s = set([a10, a11, a12]) + self.assertTrue(Article.objects.get(headline='Article 11') in s) + + def test_field_ordering(self): + """ + Field instances have a `__lt__` comparison function to define an + ordering based on their creation. Prior to #17851 this ordering + comparison relied on the now unsupported `__cmp__` and was assuming + compared objects were both Field instances raising `AttributeError` + when it should have returned `NotImplemented`. + """ + f1 = Field() + f2 = Field(auto_created=True) + f3 = Field() + self.assertTrue(f2 < f1) + self.assertTrue(f3 > f1) + self.assertFalse(f1 == None) + self.assertFalse(f2 in (None, 1, '')) + + def test_extra_method_select_argument_with_dashes_and_values(self): + # The 'select' argument to extra() supports names with dashes in + # them, as long as you use values(). + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + dicts = Article.objects.filter( + pub_date__year=2008).extra( + select={'dashed-value': '1'} + ).values('headline', 'dashed-value') + self.assertEqual([sorted(d.items()) for d in dicts], + [[('dashed-value', 1), ('headline', 'Article 11')], [('dashed-value', 1), ('headline', 'Article 12')]]) + + def test_extra_method_select_argument_with_dashes(self): + # If you use 'select' with extra() and names containing dashes on a + # query that's *not* a values() query, those extra 'select' values + # will silently be ignored. + a10 = Article.objects.create( + headline="Article 10", + pub_date=datetime(2005, 7, 31, 12, 30, 45), + ) + a11 = Article.objects.create( + headline='Article 11', + pub_date=datetime(2008, 1, 1), + ) + a12 = Article.objects.create( + headline='Article 12', + pub_date=datetime(2008, 12, 31, 23, 59, 59, 999999), + ) + + articles = Article.objects.filter( + pub_date__year=2008).extra( + select={'dashed-value': '1', 'undashedvalue': '2'}) + self.assertEqual(articles[0].undashedvalue, 2) + + def test_create_relation_with_ugettext_lazy(self): + """ + Test that ugettext_lazy objects work when saving model instances + through various methods. Refs #10498. + """ + notlazy = 'test' + lazy = ugettext_lazy(notlazy) + reporter = Article.objects.create(headline=lazy, pub_date=datetime.now()) + article = Article.objects.get() + self.assertEqual(article.headline, notlazy) + # test that assign + save works with Promise objecs + article.headline = lazy + article.save() + self.assertEqual(article.headline, notlazy) + # test .update() + Article.objects.update(headline=lazy) + article = Article.objects.get() + self.assertEqual(article.headline, notlazy) + # still test bulk_create() + Article.objects.all().delete() + Article.objects.bulk_create([Article(headline=lazy, pub_date=datetime.now())]) + article = Article.objects.get() + self.assertEqual(article.headline, notlazy) + + def test_emptyqs(self): + # Can't be instantiated + with self.assertRaises(TypeError): + EmptyQuerySet() + self.assertTrue(isinstance(Article.objects.none(), EmptyQuerySet)) + + def test_emptyqs_values(self): + # test for #15959 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + qs = Article.objects.none().values_list('pk') + self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertTrue(isinstance(qs, ValuesListQuerySet)) + self.assertEqual(len(qs), 0) + + def test_emptyqs_customqs(self): + # A hacky test for custom QuerySet subclass - refs #17271 + Article.objects.create(headline='foo', pub_date=datetime.now()) + class CustomQuerySet(QuerySet): + def do_something(self): + return 'did something' + + qs = Article.objects.all() + qs.__class__ = CustomQuerySet + qs = qs.none() + with self.assertNumQueries(0): + self.assertEqual(len(qs), 0) + self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertEqual(qs.do_something(), 'did something') + + def test_emptyqs_values_order(self): + # Tests for ticket #17712 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().values_list('id').order_by('id')), 0) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().filter( + id__in=Article.objects.values_list('id', flat=True))), 0) + + @skipUnlessDBFeature('can_distinct_on_fields') + def test_emptyqs_distinct(self): + # Tests for #19426 + Article.objects.create(headline='foo', pub_date=datetime.now()) + with self.assertNumQueries(0): + self.assertEqual(len(Article.objects.none().distinct('headline', 'pub_date')), 0) + + def test_invalid_qs_list(self): + qs = Article.objects.order_by('invalid_column') + self.assertRaises(FieldError, list, qs) + self.assertRaises(FieldError, list, qs) \ No newline at end of file -- cgit v1.3 From 6b4834952dcce0db5cbc1534635c00ff8573a6d8 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Thu, 29 Nov 2012 12:10:31 +0200 Subject: Fixed #16649 -- Refactored save_base logic Model.save() will use UPDATE - if not updated - INSERT instead of SELECT - if found UPDATE else INSERT. This should save a query when updating, but will cost a little when inserting model with PK set. Also fixed #17341 -- made sure .save() commits transactions only after the whole model has been saved. This wasn't the case in model inheritance situations. The save_base implementation was refactored into multiple methods. A typical chain for inherited save is: save_base() _save_parents(self) for each parent: _save_parents(parent) _save_table(parent) _save_table(self) --- django/db/models/base.py | 216 +++++++++++++++++++---------------- docs/ref/models/instances.txt | 13 ++- docs/releases/1.6.txt | 4 + tests/basic/tests.py | 31 ++++- tests/model_inheritance/tests.py | 2 +- tests/transactions_regress/models.py | 2 + tests/transactions_regress/tests.py | 24 +++- 7 files changed, 179 insertions(+), 113 deletions(-) (limited to 'tests/basic') diff --git a/django/db/models/base.py b/django/db/models/base.py index ab0e42d461..f3e3b76dd7 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -545,125 +545,139 @@ class Model(six.with_metaclass(ModelBase)): force_update=force_update, update_fields=update_fields) save.alters_data = True - def save_base(self, raw=False, cls=None, origin=None, force_insert=False, + def save_base(self, raw=False, force_insert=False, force_update=False, using=None, update_fields=None): """ - Does the heavy-lifting involved in saving. Subclasses shouldn't need to - override this method. It's separate from save() in order to hide the - need for overrides of save() to pass around internal-only parameters - ('raw', 'cls', and 'origin'). + Handles the parts of saving which should be done only once per save, + yet need to be done in raw saves, too. This includes some sanity + checks and signal sending. + + The 'raw' argument is telling save_base not to save any parent + models and not to do any changes to the values before save. This + is used by fixture loading. """ using = using or router.db_for_write(self.__class__, instance=self) assert not (force_insert and (force_update or update_fields)) assert update_fields is None or len(update_fields) > 0 - if cls is None: - cls = self.__class__ - meta = cls._meta - if not meta.proxy: - origin = cls - else: - meta = cls._meta - - if origin and not meta.auto_created: + cls = origin = self.__class__ + # Skip proxies, but keep the origin as the proxy model. + if cls._meta.proxy: + cls = cls._meta.concrete_model + meta = cls._meta + if not meta.auto_created: signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using, update_fields=update_fields) - - # If we are in a raw save, save the object exactly as presented. - # That means that we don't try to be smart about saving attributes - # that might have come from the parent class - we just save the - # attributes we have been given to the class we have been given. - # We also go through this process to defer the save of proxy objects - # to their actual underlying model. - if not raw or meta.proxy: - if meta.proxy: - org = cls - else: - org = None - for parent, field in meta.parents.items(): - # At this point, parent's primary key field may be unknown - # (for example, from administration form which doesn't fill - # this field). If so, fill it. - if field and getattr(self, parent._meta.pk.attname) is None and getattr(self, field.attname) is not None: - setattr(self, parent._meta.pk.attname, getattr(self, field.attname)) - - self.save_base(cls=parent, origin=org, using=using, - update_fields=update_fields) - - if field: - setattr(self, field.attname, self._get_pk_val(parent._meta)) - # Since we didn't have an instance of the parent handy, we - # set attname directly, bypassing the descriptor. - # Invalidate the related object cache, in case it's been - # accidentally populated. A fresh instance will be - # re-built from the database if necessary. - cache_name = field.get_cache_name() - if hasattr(self, cache_name): - delattr(self, cache_name) - - if meta.proxy: - return - - if not meta.proxy: - non_pks = [f for f in meta.local_fields if not f.primary_key] - - if update_fields: - non_pks = [f for f in non_pks if f.name in update_fields or f.attname in update_fields] - - with transaction.commit_on_success_unless_managed(using=using): - # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = self._get_pk_val(meta) - pk_set = pk_val is not None - record_exists = True - manager = cls._base_manager - if pk_set: - # Determine if we should do an update (pk already exists, forced update, - # no force_insert) - if ((force_update or update_fields) or (not force_insert and - manager.using(using).filter(pk=pk_val).exists())): - if force_update or non_pks: - values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] - if values: - rows = manager.using(using).filter(pk=pk_val)._update(values) - if force_update and not rows: - raise DatabaseError("Forced update did not affect any rows.") - if update_fields and not rows: - raise DatabaseError("Save with update_fields did not affect any rows.") - else: - record_exists = False - if not pk_set or not record_exists: - if meta.order_with_respect_to: - # If this is a model with an order_with_respect_to - # autopopulate the _order field - field = meta.order_with_respect_to - order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count() - self._order = order_value - - fields = meta.local_fields - if not pk_set: - if force_update or update_fields: - raise ValueError("Cannot force an update in save() with no primary key.") - fields = [f for f in fields if not isinstance(f, AutoField)] - - record_exists = False - - update_pk = bool(meta.has_auto_field and not pk_set) - result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw) - - if update_pk: - setattr(self, meta.pk.attname, result) - + with transaction.commit_on_success_unless_managed(using=using, savepoint=False): + if not raw: + self._save_parents(cls, using, update_fields) + updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields) # Store the database on which the object was saved self._state.db = using # Once saved, this is no longer a to-be-added instance. self._state.adding = False # Signal that the save is complete - if origin and not meta.auto_created: - signals.post_save.send(sender=origin, instance=self, created=(not record_exists), + if not meta.auto_created: + signals.post_save.send(sender=origin, instance=self, created=(not updated), update_fields=update_fields, raw=raw, using=using) save_base.alters_data = True + def _save_parents(self, cls, using, update_fields): + """ + Saves all the parents of cls using values from self. + """ + meta = cls._meta + for parent, field in meta.parents.items(): + # Make sure the link fields are synced between parent and self. + if (field and getattr(self, parent._meta.pk.attname) is None + and getattr(self, field.attname) is not None): + setattr(self, parent._meta.pk.attname, getattr(self, field.attname)) + self._save_parents(cls=parent, using=using, update_fields=update_fields) + self._save_table(cls=parent, using=using, update_fields=update_fields) + # Set the parent's PK value to self. + if field: + setattr(self, field.attname, self._get_pk_val(parent._meta)) + # Since we didn't have an instance of the parent handy set + # attname directly, bypassing the descriptor. Invalidate + # the related object cache, in case it's been accidentally + # populated. A fresh instance will be re-built from the + # database if necessary. + cache_name = field.get_cache_name() + if hasattr(self, cache_name): + delattr(self, cache_name) + + def _save_table(self, raw=False, cls=None, force_insert=False, + force_update=False, using=None, update_fields=None): + """ + Does the heavy-lifting involved in saving. Updates or inserts the data + for a single table. + """ + meta = cls._meta + non_pks = [f for f in meta.local_fields if not f.primary_key] + + if update_fields: + non_pks = [f for f in non_pks + if f.name in update_fields or f.attname in update_fields] + + pk_val = self._get_pk_val(meta) + pk_set = pk_val is not None + if not pk_set and (force_update or update_fields): + raise ValueError("Cannot force an update in save() with no primary key.") + updated = False + # If possible, try an UPDATE. If that doesn't update anything, do an INSERT. + if pk_set and not force_insert: + base_qs = cls._base_manager.using(using) + values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) + for f in non_pks] + if not values: + # We can end up here when saving a model in inheritance chain where + # update_fields doesn't target any field in current model. In that + # case we just say the update succeeded. Another case ending up here + # is a model with just PK - in that case check that the PK still + # exists. + updated = update_fields is not None or base_qs.filter(pk=pk_val).exists() + else: + updated = self._do_update(base_qs, using, pk_val, values) + if force_update and not updated: + raise DatabaseError("Forced update did not affect any rows.") + if update_fields and not updated: + raise DatabaseError("Save with update_fields did not affect any rows.") + if not updated: + if meta.order_with_respect_to: + # If this is a model with an order_with_respect_to + # autopopulate the _order field + field = meta.order_with_respect_to + order_value = cls._base_manager.using(using).filter( + **{field.name: getattr(self, field.attname)}).count() + self._order = order_value + + fields = meta.local_fields + if not pk_set: + fields = [f for f in fields if not isinstance(f, AutoField)] + + update_pk = bool(meta.has_auto_field and not pk_set) + result = self._do_insert(cls._base_manager, using, fields, update_pk, raw) + if update_pk: + setattr(self, meta.pk.attname, result) + return updated + + def _do_update(self, base_qs, using, pk_val, values): + """ + This method will try to update the model. If the model was updated (in + the sense that an update query was done and a matching row was found + from the DB) the method will return True. + """ + return base_qs.filter(pk=pk_val)._update(values) > 0 + + def _do_insert(self, manager, using, fields, update_pk, raw): + """ + Do an INSERT. If update_pk is defined then this method should return + the new pk for the model. + """ + return manager._insert([self], fields=fields, return_id=update_pk, + using=using, raw=raw) + def delete(self, using=None): using = using or router.db_for_write(self.__class__, instance=self) assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname) diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 92071b8d3f..9f583c42ac 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -292,12 +292,13 @@ follows this algorithm: * If the object's primary key attribute is set to a value that evaluates to ``True`` (i.e., a value other than ``None`` or the empty string), Django - executes a ``SELECT`` query to determine whether a record with the given - primary key already exists. -* If the record with the given primary key does already exist, Django - executes an ``UPDATE`` query. -* If the object's primary key attribute is *not* set, or if it's set but a - record doesn't exist, Django executes an ``INSERT``. + executes an ``UPDATE``. +* If the object's primary key attribute is *not* set or if the ``UPDATE`` + didn't update anything, Django executes an ``INSERT``. + +.. versionchanged:: 1.6 + Previously Django used ``SELECT`` - if not found ``INSERT`` else ``UPDATE`` + algorithm. The old algorithm resulted in one more query in ``UPDATE`` case. The one gotcha here is that you should be careful not to specify a primary-key value explicitly when saving new objects, if you cannot guarantee the diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 30c3cc5d2c..a44545ddf3 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -150,6 +150,10 @@ Minor features * Generic :class:`~django.contrib.gis.db.models.GeometryField` is now editable with the OpenLayers widget in the admin. +* The :meth:`Model.save() ` will do + ``UPDATE`` - if not updated - ``INSERT`` instead of ``SELECT`` - if not + found ``INSERT`` else ``UPDATE`` in case the model's primary key is set. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 2de87a225f..a8005baca7 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -1,11 +1,13 @@ from __future__ import absolute_import, unicode_literals from datetime import datetime +import threading from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError +from django.db import connections, DEFAULT_DB_ALIAS from django.db.models.fields import Field, FieldDoesNotExist from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet -from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six from django.utils.translation import ugettext_lazy @@ -690,4 +692,29 @@ class ModelTest(TestCase): def test_invalid_qs_list(self): qs = Article.objects.order_by('invalid_column') self.assertRaises(FieldError, list, qs) - self.assertRaises(FieldError, list, qs) \ No newline at end of file + self.assertRaises(FieldError, list, qs) + +class ConcurrentSaveTests(TransactionTestCase): + @skipUnlessDBFeature('test_db_allows_multiple_connections') + def test_concurrent_delete_with_save(self): + """ + Test fetching, deleting and finally saving an object - we should get + an insert in this case. + """ + a = Article.objects.create(headline='foo', pub_date=datetime.now()) + exceptions = [] + def deleter(): + try: + # Do not delete a directly - doing so alters its state. + Article.objects.filter(pk=a.pk).delete() + connections[DEFAULT_DB_ALIAS].commit_unless_managed() + except Exception as e: + exceptions.append(e) + finally: + connections[DEFAULT_DB_ALIAS].close() + self.assertEqual(len(exceptions), 0) + t = threading.Thread(target=deleter) + t.start() + t.join() + a.save() + self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 62f521c07f..dc40d2d2e0 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -294,7 +294,7 @@ class ModelInheritanceTests(TestCase): rating=4, chef=c ) - with self.assertNumQueries(6): + with self.assertNumQueries(3): ir.save() def test_update_parent_filtering(self): diff --git a/tests/transactions_regress/models.py b/tests/transactions_regress/models.py index a4b576c3ca..e09e81d93d 100644 --- a/tests/transactions_regress/models.py +++ b/tests/transactions_regress/models.py @@ -4,6 +4,8 @@ from django.db import models class Mod(models.Model): fld = models.IntegerField() +class SubMod(Mod): + cnt = models.IntegerField(unique=True) class M2mA(models.Model): others = models.ManyToManyField('M2mB') diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 142c09d3cf..e320f76169 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -1,6 +1,7 @@ from __future__ import absolute_import -from django.db import connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError +from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError, + IntegrityError) from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError from django.test import TransactionTestCase, skipUnlessDBFeature from django.test.utils import override_settings @@ -8,8 +9,25 @@ from django.utils.unittest import skipIf, skipUnless from transactions.tests import IgnorePendingDeprecationWarningsMixin -from .models import Mod, M2mA, M2mB - +from .models import Mod, M2mA, M2mB, SubMod + +class ModelInheritanceTests(TransactionTestCase): + def test_save(self): + # First, create a SubMod, then try to save another with conflicting + # cnt field. The problem was that transactions were committed after + # every parent save when not in managed transaction. As the cnt + # conflict is in the second model, we can check if the first save + # was committed or not. + SubMod(fld=1, cnt=1).save() + # We should have committed the transaction for the above - assert this. + connection.rollback() + self.assertEqual(SubMod.objects.count(), 1) + try: + SubMod(fld=2, cnt=1).save() + except IntegrityError: + connection.rollback() + self.assertEqual(SubMod.objects.count(), 1) + self.assertEqual(Mod.objects.count(), 1) class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ -- cgit v1.3 From 23490a2394bf03e119e4c9a7a09a9ba35edad430 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Wed, 20 Mar 2013 10:41:53 +0100 Subject: Revert "Fixed 19895 -- Made second iteration over invalid queryset raise an exception too" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2cd0edaa477b327024e4007c8eaf46646dcd0f21. This commit was the cause of a memory leak. See ticket for more details. Thanks Anssi Kääriäinen for identifying the source of the bug. --- django/db/models/query.py | 14 +------------- tests/basic/tests.py | 7 +------ 2 files changed, 2 insertions(+), 19 deletions(-) (limited to 'tests/basic') diff --git a/django/db/models/query.py b/django/db/models/query.py index 22c7cfba32..7ddd933772 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -104,7 +104,7 @@ class QuerySet(object): len(self) if self._result_cache is None: - self._iter = self._safe_iterator(self.iterator()) + self._iter = self.iterator() self._result_cache = [] if self._iter: return self._result_iter() @@ -341,18 +341,6 @@ class QuerySet(object): yield obj - def _safe_iterator(self, iterator): - # ensure result cache is cleared when iterating over a queryset - # raises an exception - try: - for item in iterator: - yield item - except StopIteration: - raise - except Exception: - self._result_cache = None - raise - def aggregate(self, *args, **kwargs): """ Returns a dictionary containing the calculations (aggregation) diff --git a/tests/basic/tests.py b/tests/basic/tests.py index a8005baca7..ccbb9bd423 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from datetime import datetime import threading -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connections, DEFAULT_DB_ALIAS from django.db.models.fields import Field, FieldDoesNotExist from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet @@ -689,11 +689,6 @@ class ModelTest(TestCase): with self.assertNumQueries(0): self.assertEqual(len(Article.objects.none().distinct('headline', 'pub_date')), 0) - def test_invalid_qs_list(self): - qs = Article.objects.order_by('invalid_column') - self.assertRaises(FieldError, list, qs) - self.assertRaises(FieldError, list, qs) - class ConcurrentSaveTests(TransactionTestCase): @skipUnlessDBFeature('test_db_allows_multiple_connections') def test_concurrent_delete_with_save(self): -- cgit v1.3