From 4bd55547214ae149acadffe60536c379d51e318f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 23 Jul 2013 11:38:38 +0300 Subject: [PATCH 001/117] Fixed #20782 -- qs.values().aggregate() failure In the combination of .values().aggregate() the aggregate_select_mask didn't include the aggregates added. This resulted in bogus query. Thanks to Trac alias debanshuk for report. --- django/db/models/query.py | 5 ++++- django/db/models/sql/query.py | 4 ++++ tests/aggregation/tests.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 811e917764..9dcf620448 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -274,9 +274,12 @@ class QuerySet(object): query = self.query.clone() + aggregate_names = [] for (alias, aggregate_expr) in kwargs.items(): query.add_aggregate(aggregate_expr, self.model, alias, - is_summary=True) + is_summary=True) + aggregate_names.append(alias) + query.append_aggregate_mask(aggregate_names) return query.get_aggregation(using=self.db) diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 2a9b8ef826..da0c39e1e7 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1753,6 +1753,10 @@ class Query(object): self.aggregate_select_mask = set(names) self._aggregate_select_cache = None + def append_aggregate_mask(self, names): + if self.aggregate_select_mask is not None: + self.set_aggregate_mask(set(names).union(self.aggregate_select_mask)) + def set_extra_mask(self, names): """ Set the mask of extra select items that will be returned by SELECT, diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index c635e6ebb6..dd7e994a63 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -585,3 +585,14 @@ class BaseAggregateTestCase(TestCase): "datetime.date(2008, 1, 1)" ] ) + + def test_values_aggregation(self): + # Refs #20782 + max_rating = Book.objects.values('rating').aggregate(max_rating=Max('rating')) + self.assertEqual(max_rating['max_rating'], 5) + max_books_per_rating = Book.objects.values('rating').annotate( + books_per_rating=Count('id') + ).aggregate(Max('books_per_rating')) + self.assertEqual( + max_books_per_rating, + {'books_per_rating__max': 3}) From c00a8525c6a003b57afbe6cdef243ffad397dcbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 23 Jul 2013 13:40:50 +0300 Subject: [PATCH 002/117] Fixed #20788 -- exclude() generated subquery failure "Fixed" by adding a test case, the original problem was already fixed by earlier ORM changes. Thanks to david@judicata.com for the report and test case. --- tests/queries/tests.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 87d54b637e..4bacd9a1cc 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -2910,7 +2910,7 @@ class DoubleInSubqueryTests(TestCase): self.assertQuerysetEqual( qs, [lfb1], lambda x: x) -class Ticket18785Tests(unittest.TestCase): +class Ticket18785Tests(TestCase): def test_ticket_18785(self): # Test join trimming from ticket18785 qs = Item.objects.exclude( @@ -2920,3 +2920,22 @@ class Ticket18785Tests(unittest.TestCase): ).order_by() self.assertEqual(1, str(qs.query).count('INNER JOIN')) self.assertEqual(0, str(qs.query).count('OUTER JOIN')) + + +class Ticket20788Tests(TestCase): + def test_ticket_20788(self): + Paragraph.objects.create() + paragraph = Paragraph.objects.create() + page = paragraph.page.create() + chapter = Chapter.objects.create(paragraph=paragraph) + Book.objects.create(chapter=chapter) + + paragraph2 = Paragraph.objects.create() + Page.objects.create() + chapter2 = Chapter.objects.create(paragraph=paragraph2) + book2 = Book.objects.create(chapter=chapter2) + + sentences_not_in_pub = Book.objects.exclude( + chapter__paragraph__page=page) + self.assertQuerysetEqual( + sentences_not_in_pub, [book2], lambda x: x) From 6b4967e88368934dbbb1f289c790ab813fa59c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Tue, 23 Jul 2013 15:06:02 +0300 Subject: [PATCH 003/117] Minor change to get_extra_descriptor_filter() Refs #20611. --- django/db/models/fields/related.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 1034d8b2ac..e7683c6aec 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -296,10 +296,15 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec params = dict( (rh_field.attname, getattr(instance, lh_field.attname)) for lh_field, rh_field in self.field.related_fields) - params.update(self.field.get_extra_descriptor_filter(instance)) qs = self.get_queryset(instance=instance) + extra_filter = self.field.get_extra_descriptor_filter(instance) + if isinstance(extra_filter, dict): + params.update(extra_filter) + qs = qs.filter(**params) + else: + qs = qs.filter(extra_filter, **params) # Assuming the database enforces foreign keys, this won't fail. - rel_obj = qs.get(**params) + rel_obj = qs.get() if not self.field.rel.multiple: setattr(rel_obj, self.field.related.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) @@ -1003,10 +1008,11 @@ class ForeignObject(RelatedField): user does 'instance.fieldname', that is the extra filter is used in the descriptor of the field. - The filter should be something usable in .filter(**kwargs) call, and - will be ANDed together with the joining columns condition. + The filter should be either a dict usable in .filter(**kwargs) call or + a Q-object. The condition will be ANDed together with the relation's + joining columns. - A parallel method is get_extra_relation_restriction() which is used in + A parallel method is get_extra_restriction() which is used in JOIN and subquery conditions. """ return {} From 33242fe015a9519748cd328939dca3136f462344 Mon Sep 17 00:00:00 2001 From: Kirill Fomichev Date: Wed, 24 Oct 2012 21:11:41 +0600 Subject: [PATCH 004/117] Fixed #19019 -- Fixed UserAdmin to log password change. Thanks Tuttle for the report. --- django/contrib/auth/admin.py | 2 + django/contrib/auth/forms.py | 8 +++ django/contrib/auth/tests/test_views.py | 66 +++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index ca660606e5..e7cf8a3c64 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -127,6 +127,8 @@ class UserAdmin(admin.ModelAdmin): form = self.change_password_form(user, request.POST) if form.is_valid(): form.save() + change_message = self.construct_change_message(request, form, None) + self.log_change(request, request.user, change_message) msg = ugettext('Password changed successfully.') messages.success(request, msg) return HttpResponseRedirect('..') diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 2d7a7c14d4..bf58d0e903 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -350,3 +350,11 @@ class AdminPasswordChangeForm(forms.Form): if commit: self.user.save() return self.user + + def _get_changed_data(self): + data = super(AdminPasswordChangeForm, self).changed_data + for name in self.fields.keys(): + if name not in data: + return [] + return ['password'] + changed_data = property(_get_changed_data) diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index b939dff058..a26d477aa0 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -8,6 +8,7 @@ except ImportError: # Python 2 from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite +from django.contrib.admin.models import LogEntry from django.contrib.auth.models import User from django.core import mail from django.core.urlresolvers import reverse, NoReverseMatch @@ -54,6 +55,11 @@ class AuthViewsTestCase(TestCase): self.assertTrue(SESSION_KEY in self.client.session) return response + def logout(self): + response = self.client.get('/admin/logout/') + self.assertEqual(response.status_code, 200) + self.assertTrue(SESSION_KEY not in self.client.session) + def assertFormError(self, response, error): """Assert that error is found in response.context['form'] errors""" form_errors = list(itertools.chain(*response.context['form'].errors.values())) @@ -670,18 +676,70 @@ class LogoutTest(AuthViewsTestCase): self.confirm_logged_out() @skipIfCustomUser +@override_settings( + PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), +) class ChangelistTests(AuthViewsTestCase): urls = 'django.contrib.auth.tests.urls_admin' + def setUp(self): + # Make me a superuser before logging in. + User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True) + self.login() + self.admin = User.objects.get(pk=1) + + def get_user_data(self, user): + return { + 'username': user.username, + 'password': user.password, + 'email': user.email, + 'is_active': user.is_active, + 'is_staff': user.is_staff, + 'is_superuser': user.is_superuser, + 'last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'last_login_1': user.last_login.strftime('%H:%M:%S'), + 'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'initial-last_login_1': user.last_login.strftime('%H:%M:%S'), + 'date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'first_name': user.first_name, + 'last_name': user.last_name, + } + # #20078 - users shouldn't be allowed to guess password hashes via # repeated password__startswith queries. def test_changelist_disallows_password_lookups(self): - # Make me a superuser before loging in. - User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True) - self.login() - # A lookup that tries to filter on password isn't OK with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: response = self.client.get('/admin/auth/user/?password__startswith=sha1$') self.assertEqual(response.status_code, 400) self.assertEqual(len(logger_calls), 1) + + def test_user_change_email(self): + data = self.get_user_data(self.admin) + data['email'] = 'new_' + data['email'] + response = self.client.post('/admin/auth/user/%s/' % self.admin.pk, data) + self.assertRedirects(response, '/admin/auth/user/') + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed email.') + + def test_user_not_change(self): + response = self.client.post('/admin/auth/user/%s/' % self.admin.pk, + self.get_user_data(self.admin) + ) + self.assertRedirects(response, '/admin/auth/user/') + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'No fields changed.') + + def test_user_change_password(self): + response = self.client.post('/admin/auth/user/%s/password/' % self.admin.pk, { + 'password1': 'password1', + 'password2': 'password1', + }) + self.assertRedirects(response, '/admin/auth/user/%s/' % self.admin.pk) + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed password.') + self.logout() + self.login(password='password1') From e716518ad29898fb25c820023aaf2fdd1c9eb4af Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Sun, 21 Jul 2013 21:57:30 +0700 Subject: [PATCH 005/117] Fixed #20761 -- Fixed DatabaseError handling in get_or_create and update_or_create. --- django/db/models/query.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 9dcf620448..9e0dd9202c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -8,7 +8,7 @@ import sys from django.conf import settings from django.core import exceptions -from django.db import connections, router, transaction, DatabaseError +from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import AutoField from django.db.models.query_utils import (Q, select_related_descend, @@ -397,34 +397,35 @@ class QuerySet(object): return obj, created for k, v in six.iteritems(filtered_defaults): setattr(obj, k, v) + + sid = transaction.savepoint(using=self.db) try: - sid = transaction.savepoint(using=self.db) obj.save(update_fields=filtered_defaults.keys(), using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, False except DatabaseError: transaction.savepoint_rollback(sid, using=self.db) - six.reraise(sys.exc_info()) + six.reraise(*sys.exc_info()) def _create_object_from_params(self, lookup, params): """ Tries to create an object using passed params. Used by get_or_create and update_or_create """ + obj = self.model(**params) + sid = transaction.savepoint(using=self.db) try: - obj = self.model(**params) - sid = transaction.savepoint(using=self.db) obj.save(force_insert=True, using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, True - except DatabaseError: + except DatabaseError as e: transaction.savepoint_rollback(sid, using=self.db) - exc_info = sys.exc_info() - try: - return self.get(**lookup), False - except self.model.DoesNotExist: - # Re-raise the DatabaseError with its original traceback. - six.reraise(*exc_info) + if isinstance(e, IntegrityError): + try: + return self.get(**lookup), False + except self.model.DoesNotExist: + pass + six.reraise(*sys.exc_info()) def _extract_model_params(self, defaults, **kwargs): """ From 5dbca13f3baa2e1bafd77e84a80ad6d8a074712e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 23 Jul 2013 15:41:09 +0200 Subject: [PATCH 006/117] Fixed #20760 -- Reduced timing variation in ModelBackend. Thanks jpaglier and erikr. --- django/contrib/auth/backends.py | 4 ++- .../contrib/auth/tests/test_auth_backends.py | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 6b31f72b03..cb79291c17 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -17,7 +17,9 @@ class ModelBackend(object): if user.check_password(password): return user except UserModel.DoesNotExist: - return None + # Run the default password hasher once to reduce the timing + # difference between an existing and a non-existing user (#20760). + UserModel().set_password(password) def get_group_permissions(self, user_obj, obj=None): """ diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py index fc5a80e8dd..b48df91cfb 100644 --- a/django/contrib/auth/tests/test_auth_backends.py +++ b/django/contrib/auth/tests/test_auth_backends.py @@ -12,6 +12,17 @@ from django.contrib.auth import authenticate, get_user from django.http import HttpRequest from django.test import TestCase from django.test.utils import override_settings +from django.contrib.auth.hashers import MD5PasswordHasher + + +class CountingMD5PasswordHasher(MD5PasswordHasher): + """Hasher that counts how many times it computes a hash.""" + + calls = 0 + + def encode(self, *args, **kwargs): + type(self).calls += 1 + return super(CountingMD5PasswordHasher, self).encode(*args, **kwargs) class BaseModelBackendTest(object): @@ -107,10 +118,22 @@ class BaseModelBackendTest(object): self.assertEqual(user.get_all_permissions(), set(['auth.test'])) def test_get_all_superuser_permissions(self): - "A superuser has all permissions. Refs #14795" + """A superuser has all permissions. Refs #14795.""" user = self.UserModel._default_manager.get(pk=self.superuser.pk) self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.tests.test_auth_backends.CountingMD5PasswordHasher',)) + def test_authentication_timing(self): + """Hasher is run once regardless of whether the user exists. Refs #20760.""" + CountingMD5PasswordHasher.calls = 0 + username = getattr(self.user, self.UserModel.USERNAME_FIELD) + authenticate(username=username, password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + + CountingMD5PasswordHasher.calls = 0 + authenticate(username='no_such_user', password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + @skipIfCustomUser class ModelBackendTest(BaseModelBackendTest, TestCase): From fb052b528ad5f0a92f7420ab8ade16462b6435fd Mon Sep 17 00:00:00 2001 From: Pedro Mourelle Date: Mon, 25 Feb 2013 03:18:27 -0300 Subject: [PATCH 007/117] Fixed #19900 -- Updated admin buttons to use CSS3 rounded corners. --- .../contrib/admin/static/admin/css/base.css | 32 ++++++------------ .../admin/static/admin/css/widgets.css | 8 ++--- .../admin/static/admin/img/chooser-bg.gif | Bin 199 -> 0 bytes .../static/admin/img/chooser_stacked-bg.gif | Bin 212 -> 0 bytes .../admin/static/admin/img/tool-left.gif | Bin 197 -> 0 bytes .../admin/static/admin/img/tool-left_over.gif | Bin 203 -> 0 bytes .../admin/static/admin/img/tool-right.gif | Bin 198 -> 0 bytes .../static/admin/img/tool-right_over.gif | Bin 200 -> 0 bytes .../admin/static/admin/img/tooltag-add.gif | Bin 932 -> 0 bytes .../admin/static/admin/img/tooltag-add.png | Bin 0 -> 967 bytes .../static/admin/img/tooltag-add_over.gif | Bin 336 -> 0 bytes .../static/admin/img/tooltag-arrowright.gif | Bin 351 -> 0 bytes .../static/admin/img/tooltag-arrowright.png | Bin 0 -> 1063 bytes .../admin/img/tooltag-arrowright_over.gif | Bin 354 -> 0 bytes docs/releases/1.7.txt | 3 ++ 15 files changed, 17 insertions(+), 26 deletions(-) delete mode 100644 django/contrib/admin/static/admin/img/chooser-bg.gif delete mode 100644 django/contrib/admin/static/admin/img/chooser_stacked-bg.gif delete mode 100644 django/contrib/admin/static/admin/img/tool-left.gif delete mode 100644 django/contrib/admin/static/admin/img/tool-left_over.gif delete mode 100644 django/contrib/admin/static/admin/img/tool-right.gif delete mode 100644 django/contrib/admin/static/admin/img/tool-right_over.gif delete mode 100644 django/contrib/admin/static/admin/img/tooltag-add.gif create mode 100644 django/contrib/admin/static/admin/img/tooltag-add.png delete mode 100644 django/contrib/admin/static/admin/img/tooltag-add_over.gif delete mode 100644 django/contrib/admin/static/admin/img/tooltag-arrowright.gif create mode 100644 django/contrib/admin/static/admin/img/tooltag-arrowright.png delete mode 100644 django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index 1439b5d675..9fef3a8bc1 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -661,45 +661,34 @@ a.deletelink:hover { .object-tools li { display: block; float: left; - background: url(../img/tool-left.gif) 0 0 no-repeat; - padding: 0 0 0 8px; - margin-left: 2px; + margin-left: 5px; height: 16px; } -.object-tools li:hover { - background: url(../img/tool-left_over.gif) 0 0 no-repeat; +.object-tools a { + border-radius: 15px; } .object-tools a:link, .object-tools a:visited { display: block; float: left; color: white; - padding: .1em 14px .1em 8px; - height: 14px; - background: #999 url(../img/tool-right.gif) 100% 0 no-repeat; + padding: .2em 10px; + background: #999; } .object-tools a:hover, .object-tools li:hover a { - background: #5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat; + background-color: #5b80b2; } .object-tools a.viewsitelink, .object-tools a.golink { - background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat; - padding-right: 28px; -} - -.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { - background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat; + background: #999 url(../img/tooltag-arrowright.png) 95% center no-repeat; + padding-right: 26px; } .object-tools a.addlink { - background: #999 url(../img/tooltag-add.gif) top right no-repeat; - padding-right: 28px; -} - -.object-tools a.addlink:hover { - background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat; + background: #999 url(../img/tooltag-add.png) 95% center no-repeat; + padding-right: 26px; } /* OBJECT HISTORY */ @@ -837,4 +826,3 @@ table#change-history tbody th { background: #eee url(../img/nav-bg.gif) bottom left repeat-x; color: #666; } - diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index d61cd3a218..56817228f3 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -54,8 +54,8 @@ .selector ul.selector-chooser { float: left; width: 22px; - height: 50px; - background: url(../img/chooser-bg.gif) top center no-repeat; + background-color: #eee; + border-radius: 10px; margin: 10em 5px 0 5px; padding: 0; } @@ -169,7 +169,8 @@ a.active.selector-clearall { height: 22px; width: 50px; margin: 0 0 3px 40%; - background: url(../img/chooser_stacked-bg.gif) top center no-repeat; + background-color: #eee; + border-radius: 10px; } .stacked .selector-chooser li { @@ -575,4 +576,3 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { font-size: 11px; border-top: 1px solid #ddd; } - diff --git a/django/contrib/admin/static/admin/img/chooser-bg.gif b/django/contrib/admin/static/admin/img/chooser-bg.gif deleted file mode 100644 index 30e83c2518b0e5ee5f4bda6ce790b47f6dbdbfbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 199 zcmZ?wbhEHb6l2h5*v!E2@87@o@85s?`0?-Gzu&)q|NQy$j~_oiefsp_!-t2MMY9TZUv9~%Sp!>_)cW&ci3#* zUhC5m*S~nZz=*>R=VoO!-wlwA$Z`-01 q@~q#j3=%veq*@rvq^WHhU}-`1SSm{{H^+^Yi-p`uF$u@bK{c{r&s< z`||Sg^z`)c@$vTd_5c6?A^8LV00000EC2ui05AX-000DmFvuy15jgA3yCGB Oo}Zwd4gw#T5db^5xM#Bf diff --git a/django/contrib/admin/static/admin/img/tool-left.gif b/django/contrib/admin/static/admin/img/tool-left.gif deleted file mode 100644 index 011490ff3a0100bea63eca7d8a3f821edecf6d3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197 zcmV;$06PCiNk%w1VF>^d0K@+9+1 z>Fn(6>gwvFqods1+`_`bsi~>N#Kh<4=efDLot>Sks;aTEvEJU^(b3W4lL@fC0syEMTS%hy>Zm!0PcpEpJwG;hh<#5^Y_P4;QTzJF~^?;ZNVo;-~tj z3GxIURaQ?9NK-h~-O_%Ehr|6W>p_W03|&Y3moVBW-4OX{v_eKqd6L;=PDTc60DL-S AivR!s diff --git a/django/contrib/admin/static/admin/img/tool-right.gif b/django/contrib/admin/static/admin/img/tool-right.gif deleted file mode 100644 index cdc140cc590a56bf45ceef6eaeebf47e4a699ac3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198 zcmV;%06G6hNk%w1VF>^d0K@+9+1 z>Fn(6>gwvFqods1+`_`bsi~>N#Kh<4=efDLot>Sks;aTEvEJU^(b3W4R diff --git a/django/contrib/admin/static/admin/img/tool-right_over.gif b/django/contrib/admin/static/admin/img/tool-right_over.gif deleted file mode 100644 index 4db977e838dd97ae4f59524a764cf8298f19ccc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmZ?wbhEHblL@fC0syEMTS%hy>Zm!0M`SJhE@w+}Rz8zHMDu50@FVv!<=}o0a$2j_;|E wNJ^Lce8rW`28j}!f$kDtvUrX>)$-C{u;f$x#4um1{ZvBtsg!MMAsh_W0PS95AOHXW diff --git a/django/contrib/admin/static/admin/img/tooltag-add.gif b/django/contrib/admin/static/admin/img/tooltag-add.gif deleted file mode 100644 index 8b53d49ae58dbc324ca7fb318198b187fc124c09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 932 zcmZ?wbhEHb6lM@)_|Cxa`}glZfBv+zw0!^m{n4XG%aU|5d-nPB=MNq{xOeZ~wr$(``}?0g zdp3Xm{Fg6Zwzjt3y?giHzkgr9e*ORde?vn9!zdUHfx#C7ia%Mvj?w{lD0p`BHNuZH8`gv5jGtilW#8Ul-s@(Rm3u`n_%YH1afFWM53xbWyi0reSoZU`=F zW>B;|HRonfOG771<|UO1;}pj!{KgMbG6N3=Of=;-YN#}Nd4NeFEUKo%!I@!YFuRjN h0z*m|qaatng9}?1vhrtKUbNTY0V7A3A`=sXH2_xRgyR4J diff --git a/django/contrib/admin/static/admin/img/tooltag-add.png b/django/contrib/admin/static/admin/img/tooltag-add.png new file mode 100644 index 0000000000000000000000000000000000000000..f352cf590fa7cbf346fb51203b2c55025da93941 GIT binary patch literal 967 zcmaJ=&ui0A91k6iF&ykB6TwHOpkVWBO=d!4*EX%IETyysd-OGVZ9|*9`0{GA9YlAV z_)mD+P4FfNIs^}bAb9j9{sAI_r{R3*+HMDHNb=tM^7(weKi@&EdUs{{@-oM9E0t}l z&eqlBTUuoQtG`YlTh?f)MH{$7N01;c@8UKB6(4p{9YJ^Z;YXC^xP@z8vqf9>j^SWm zfC)y3{gAOaE}M%(==2c4KMIMYHv~TjKLAC4}C}M(uDr}@3NtS`8335i&H2D@# zBv}z9_G)Q4W2l;;Y=X(dGjGK07W@x@Li-=j1%L~%4438M`GlddT1x}In#N}3_k z(QZItoDQP(se*+fhj=0NZ~ziT*v3O@@+{KR6#Q@|8$^>bv4V*)3`JRxl9Z-E+x|b) z_h;yc*3ntK{}hgzyCD+mD8fVHu*P-P6IY>8BnVPWnivnJRjl#D9QnyTkZmXs+?xfXVY zJ__iR>z;9obGgYd_#yLbA>utkZkb>ojFUFJ`LXD8^(I_*ek}R9T#*GMCapc!>Zys% zPqLj2E<4Od9|dgQ2^;KhhuYKOAMJnm w{Q7-rP5Scb!t>jM&XxSDqb;zr_~H_my2!mfT;h+P-6P4>s+6kM%ff^GzbQ5?qyPW_ literal 0 HcmV?d00001 diff --git a/django/contrib/admin/static/admin/img/tooltag-add_over.gif b/django/contrib/admin/static/admin/img/tooltag-add_over.gif deleted file mode 100644 index bfc52f10de75998687154585752513a27a02e5c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 336 zcmZ?wbhEHb6lM@)xXQp_AKmol&!20zAN~IQ+b5;fJ+bBa>km(!zx7XVkIL_<>s=I* z-Fg4<%i9m06*kQ}fA#LqpFhjm=B8Ioc1vhpx_)m~&D4mz?loHv1!s0R#56sA{$|&q zv$L0NTfOzr<~=7olUmZNro(_t( z{<*|8FIl(u()IiQ|Npm*YGNP-DE?#tE7t*$AU`p%OuD z64&(xJYQ3LjTSFY75~+{EbMWTq?Tj+^`lM+ch;D9HZb0eJO28D|1uloD@pFe*-apJ^Hn>L+2d-m?#yFEQU|Ni~^ z`t|Go|Nk2r8W_k0ia%MvT6I7q$WIJxH4gmQ9x7=Hf&p1aV>r7mD6wA%agA(z(Y9-o zn{Z!7=F3()sh$@ap>8_^S=AZ45`|}1urBhDn)0zi%$1R$zCpXHxz#1VpeR78J1{*n zKx$GzLbz~Ucw``(R#|u;i#+q?x}l#o5$^*7-1rs_)vpOO{(vYlW{PgEasf C+m{mn diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright.png b/django/contrib/admin/static/admin/img/tooltag-arrowright.png new file mode 100644 index 0000000000000000000000000000000000000000..ac1ac5b89a27be2ba972c588fec3c2b33c6a5de7 GIT binary patch literal 1063 zcmaJ=&rcIU6ka4jL{SqA20buYG?7TVKiV$r(h8;9N{Nf4G_eN{F1u5@p}Vv04z_T> zLX3tRHzhF<W z0iVWl91sLQ7Yqpk*9Ul(;~AE`f{zPIAwlB%!Qn%bXpWwhQgY%j7O^694r5zlm_ni8 zF9iI^nPxaq6g>@|_Ys88owKl7^jU6CO+kjP<`_0MkOe$NbqdYm2u(6QID%={Wi9ux zOr&5;QMDP)&w59y0Tt!{P}8iVU7Uh{^8KfyYpmSPT6F>=xf zeh|R1Yl~jdPJ3 z>K$_RhPedd4Y>?ShVfeaSF39-GC$t7KDflFk3O`>ygOvDQ!5*Jaw%4max`81GWBbJ zf2jFlX?byEIMCclahqtrG|=|y*c$tG>GNWxe7CjIR?ZG=eG5|eV5!o(weoJbPkG)1 z@+ZqfPw$s*mZQXYTkm(!zx7XVkIL_<>s=I* z-Fg4<%i9m06*kQ}fA#LqpFhjm=B8Ioc1vhpx_)m~&D4mz?loHv1!s0R#56sA{$|&q zv$L0NTfOzr<~=7olUmZNro(_t( z{<*|8FIl(u()IiQ|Npm*YGNP-DE?#tE7t*$AU`p%)jRO!c&Mb=iC%tqw1%_mf>OuD z64%5AjkaH#++@37WWH>5lj_LG3QgM?$)(Pyl_<=6_Sxb99hOrSl|Hhr49pEp`qi!N zF8KvT0m8k3>6rnFQvwpgW$MEt13C4|!UI|47?&<{5fc~Cj$Ol`%&8*1!G}pzckjNv NA{t6st9%_9tO2syk|zKF diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 8c5a0fb585..a7ab751dd4 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -72,6 +72,9 @@ Minor features allowing the ``published`` element to be included in the feed (which relies on ``pubdate``). +* Buttons in :mod:`django.contrib.admin` now use the ``border-radius`` CSS + property for rounded corners rather than GIF background images. + Backwards incompatible changes in 1.7 ===================================== From ad98b985aa18fbca65b45aae2421b985904bf561 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 23 Jul 2013 15:20:53 -0400 Subject: [PATCH 008/117] Fixed test failures introduced in e716518ad29898fb25c820023aaf2fdd1c9eb4af refs #20761 --- django/db/models/query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 9e0dd9202c..087c10de8e 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -420,12 +420,13 @@ class QuerySet(object): return obj, True except DatabaseError as e: transaction.savepoint_rollback(sid, using=self.db) + exc_info = sys.exc_info() if isinstance(e, IntegrityError): try: return self.get(**lookup), False except self.model.DoesNotExist: pass - six.reraise(*sys.exc_info()) + six.reraise(*exc_info) def _extract_model_params(self, defaults, **kwargs): """ From 311c1d2848bde59bf03627e5c3d72b319285201b Mon Sep 17 00:00:00 2001 From: ersran9 Date: Tue, 23 Jul 2013 23:02:09 +0530 Subject: [PATCH 009/117] Fixed #20761 -- Reworded ForeignKey default error message --- django/db/models/fields/related.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index e7683c6aec..11a20983eb 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1125,7 +1125,7 @@ class ForeignObject(RelatedField): class ForeignKey(ForeignObject): empty_strings_allowed = False default_error_messages = { - 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') + 'invalid': _('%(model)s instance with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") From 65e03a424e82e157b4513cdebb500891f5c78363 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 12 Jul 2013 09:52:18 -0400 Subject: [PATCH 010/117] Fixed #10284 -- ModelFormSet.save(commit=False) no longer deletes objects Thanks laureline.guerin@ and Wedg. --- django/forms/models.py | 3 ++- docs/releases/1.7.txt | 5 +++++ docs/topics/forms/formsets.txt | 34 ++++++++++++++++++++++++-------- docs/topics/forms/modelforms.txt | 7 +++++++ tests/model_formsets/tests.py | 3 +++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 1d3bec1635..2d1ec96306 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -717,7 +717,8 @@ class BaseModelFormSet(BaseFormSet): obj = self._existing_object(pk_value) if form in forms_to_delete: self.deleted_objects.append(obj) - obj.delete() + if commit: + obj.delete() continue if form.has_changed(): self.changed_objects.append((obj, form.changed_data)) diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index a7ab751dd4..bec24c94dc 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -94,6 +94,11 @@ Miscellaneous have a custom :class:`~django.core.files.uploadhandler.FileUploadHandler` that implements ``new_file()``, be sure it accepts this new parameter. +* :class:`ModelFormSet`'s no longer + delete instances when ``save(commit=False)`` is called. See + :attr:`~django.forms.formsets.BaseFormSet.can_delete` for instructions on how + to manually delete objects from deleted forms. + Features deprecated in 1.7 ========================== diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 470d9f52e4..8be6e10ff6 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -3,7 +3,10 @@ Formsets ======== -.. class:: django.forms.formsets.BaseFormSet +.. module:: django.forms.formsets + :synopsis: An abstraction for working with multiple forms on the same page. + +.. class:: BaseFormSet A formset is a layer of abstraction to work with multiple forms on the same page. It can be best compared to a data grid. Let's say you have the following @@ -164,9 +167,7 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the forms in the formset. Validation was performed for each of the two forms, and the expected error message appears for the second item. -.. currentmodule:: django.forms.formsets.BaseFormSet - -.. method:: total_error_count(self) +.. method:: BaseFormSet.total_error_count(self) .. versionadded:: 1.6 @@ -353,6 +354,8 @@ formsets and deletion of forms from a formset. ``can_order`` ~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_order + Default: ``False`` Lets you create a formset with the ability to order:: @@ -411,6 +414,8 @@ happen when the user changes these values:: ``can_delete`` ~~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_delete + Default: ``False`` Lets you create a formset with the ability to select forms for deletion:: @@ -463,10 +468,23 @@ delete fields you can access them with ``deleted_forms``:: If you are using a :class:`ModelFormSet`, model instances for deleted forms will be deleted when you call -``formset.save()``. On the other hand, if you are using a plain ``FormSet``, -it's up to you to handle ``formset.deleted_forms``, perhaps in your formset's -``save()`` method, as there's no general notion of what it means to delete a -form. +``formset.save()``. + +.. versionchanged:: 1.7 + + If you call ``formset.save(commit=False)``, objects will not be deleted + automatically. You'll need to call ``delete()`` on each of the + :attr:`formset.deleted_objects + ` to actually delete + them:: + + >>> instances = formset.save(commit=False) + >>> for obj in formset.deleted_objects: + ... obj.delete() + +On the other hand, if you are using a plain ``FormSet``, it's up to you to +handle ``formset.deleted_forms``, perhaps in your formset's ``save()`` method, +as there's no general notion of what it means to delete a form. Adding additional fields to a formset ------------------------------------- diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 0f3c5bb815..4ef7a6f074 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -825,6 +825,13 @@ to the database. If your formset contains a ``ManyToManyField``, you'll also need to call ``formset.save_m2m()`` to ensure the many-to-many relationships are saved properly. +After calling ``save()``, your model formset will have three new attributes +containing the formset's changes: + +.. attribute:: models.BaseModelFormSet.changed_objects +.. attribute:: models.BaseModelFormSet.deleted_objects +.. attribute:: models.BaseModelFormSet.new_objects + .. _model-formsets-max-num: Limiting the number of editable objects diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 4411bbf59a..f66b5abb63 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -32,6 +32,9 @@ class DeletionTests(TestCase): 'form-0-DELETE': 'on', } formset = PoetFormSet(data, queryset=Poet.objects.all()) + formset.save(commit=False) + self.assertEqual(Poet.objects.count(), 1) + formset.save() self.assertTrue(formset.is_valid()) self.assertEqual(Poet.objects.count(), 0) From c928725b933ff479c04e8a7fb74c4dc2ba138aa7 Mon Sep 17 00:00:00 2001 From: Dominic Rodger Date: Tue, 23 Jul 2013 21:58:43 +0100 Subject: [PATCH 011/117] Fixed #20794 -- Documented changes to validate_email 4e2e8f39d changed the way validate_email behaves for foo@localhost email addresses, but wasn't listed in the release notes. --- docs/releases/1.6.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b8babe1843..5b3c7903c5 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -801,6 +801,9 @@ Miscellaneous of the admin views. You should update your custom templates if they use the previous parameter name. +* :meth:`~django.core.validators.validate_email` now accepts email addresses + with ``localhost`` as the domain. + Features deprecated in 1.6 ========================== From 31c13a99bb9ebdaf12ccab4e880c5da930d86e79 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 16 Jul 2013 09:10:04 -0400 Subject: [PATCH 012/117] Fixed #14300 -- Fixed initial SQL location if models is a package. Thanks al_the_x for the report and fheinz for the draft patch. --- django/core/management/commands/loaddata.py | 2 +- django/core/management/sql.py | 20 ++++++++++++++++--- django/db/models/__init__.py | 2 +- django/db/models/loading.py | 16 +++++++++++---- docs/internals/deprecation.txt | 4 ++++ docs/releases/1.7.txt | 9 +++++++++ .../models/sql/book.sql | 2 ++ tests/fixtures_model_package/sql/book.sql | 1 + tests/fixtures_model_package/tests.py | 17 ++++++++++++++++ 9 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures_model_package/models/sql/book.sql create mode 100644 tests/fixtures_model_package/sql/book.sql diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 6856e85e45..aa879f6acc 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -233,7 +233,7 @@ class Command(BaseCommand): """ dirs = [] for path in get_app_paths(): - d = os.path.join(os.path.dirname(path), 'fixtures') + d = os.path.join(path, 'fixtures') if os.path.isdir(d): dirs.append(d) dirs.extend(list(settings.FIXTURE_DIRS)) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index b58d89f60a..c5806086f9 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import codecs import os import re +import warnings from django.conf import settings from django.core.management.base import CommandError @@ -168,7 +169,18 @@ def _split_statements(content): def custom_sql_for_model(model, style, connection): opts = model._meta - app_dir = os.path.normpath(os.path.join(os.path.dirname(upath(models.get_app(model._meta.app_label).__file__)), 'sql')) + app_dirs = [] + app_dir = models.get_app_path(model._meta.app_label) + app_dirs.append(os.path.normpath(os.path.join(app_dir, 'sql'))) + + # Deprecated location -- remove in Django 1.9 + old_app_dir = os.path.normpath(os.path.join(app_dir, 'models/sql')) + if os.path.exists(old_app_dir): + warnings.warn("Custom SQL location '/models/sql' is " + "deprecated, use '/sql' instead.", + PendingDeprecationWarning) + app_dirs.append(old_app_dir) + output = [] # Post-creation SQL should come before any initial SQL data is loaded. @@ -181,8 +193,10 @@ def custom_sql_for_model(model, style, connection): # Find custom SQL, if it's available. backend_name = connection.settings_dict['ENGINE'].split('.')[-1] - sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)), - os.path.join(app_dir, "%s.sql" % opts.model_name)] + sql_files = [] + for app_dir in app_dirs: + sql_files.append(os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name))) + sql_files.append(os.path.join(app_dir, "%s.sql" % opts.model_name)) for sql_file in sql_files: if os.path.exists(sql_file): with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 4d310e480b..33151e068d 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,7 +1,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp +from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp from django.db.models.query import Q from django.db.models.expressions import F from django.db.models.manager import Manager diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 7280051bd8..9a0cadaf37 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -154,6 +154,16 @@ class AppCache(object): return [elt[0] for elt in apps] + def _get_app_path(self, app): + if hasattr(app, '__path__'): # models/__init__.py package + app_path = app.__path__[0] + else: # models.py module + app_path = app.__file__ + return os.path.dirname(upath(app_path)) + + def get_app_path(self, app_label): + return self._get_app_path(self.get_app(app_label)) + def get_app_paths(self): """ Returns a list of paths to all installed apps. @@ -165,10 +175,7 @@ class AppCache(object): app_paths = [] for app in self.get_apps(): - if hasattr(app, '__path__'): # models/__init__.py package - app_paths.extend([upath(path) for path in app.__path__]) - else: # models.py module - app_paths.append(upath(app.__file__)) + app_paths.append(self._get_app_path(app)) return app_paths def get_app(self, app_label, emptyOK=False): @@ -321,6 +328,7 @@ cache = AppCache() # These methods were always module level, so are kept that way for backwards # compatibility. get_apps = cache.get_apps +get_app_path = cache.get_app_path get_app_paths = cache.get_app_paths get_app = cache.get_app get_app_errors = cache.get_app_errors diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b0f5566cb3..7f93e1dc58 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -414,6 +414,10 @@ these changes. * ``django.utils.unittest`` will be removed. +* If models are organized in a package, Django will no longer look for + :ref:`initial SQL data` in ``myapp/models/sql/``. Move your + custom SQL files to ``myapp/sql/``. + 2.0 --- diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index bec24c94dc..bec5aaa12a 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -116,3 +116,12 @@ on all Python versions. Since ``unittest2`` became the standard library's :mod:`unittest` module in Python 2.7, and Django 1.7 drops support for older Python versions, this module isn't useful anymore. It has been deprecated. Use :mod:`unittest` instead. + +Custom SQL location for models package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, if models were organized in a package (``myapp/models/``) rather +than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data +` in ``myapp/models/sql/``. This bug has been fixed so that Django +will search ``myapp/sql/`` as documented. The old location will continue to +work until Django 1.9. diff --git a/tests/fixtures_model_package/models/sql/book.sql b/tests/fixtures_model_package/models/sql/book.sql new file mode 100644 index 0000000000..9b3918f4d7 --- /dev/null +++ b/tests/fixtures_model_package/models/sql/book.sql @@ -0,0 +1,2 @@ +-- Deprecated search path for custom SQL -- remove in Django 1.9 +INSERT INTO fixtures_model_package_book (name) VALUES ('My Deprecated Book'); diff --git a/tests/fixtures_model_package/sql/book.sql b/tests/fixtures_model_package/sql/book.sql new file mode 100644 index 0000000000..21b1d9465b --- /dev/null +++ b/tests/fixtures_model_package/sql/book.sql @@ -0,0 +1 @@ +INSERT INTO fixtures_model_package_book (name) VALUES ('My Book'); diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index ad82267da3..1e22ac9833 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -5,6 +5,7 @@ import warnings from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase +from django.utils.six import StringIO from .models import Article, Book @@ -110,3 +111,19 @@ class FixtureTestCase(TestCase): ], lambda a: a.headline, ) + + +class InitialSQLTests(TestCase): + + def test_custom_sql(self): + """ + #14300 -- Verify that custom_sql_for_model searches `app/sql` and not + `app/models/sql` (the old location will work until Django 1.9) + """ + out = StringIO() + management.call_command("sqlcustom", "fixtures_model_package", stdout=out) + output = out.getvalue() + self.assertTrue("INSERT INTO fixtures_model_package_book (name) " + "VALUES ('My Book')" in output) + # value from deprecated search path models/sql (remove in Django 1.9) + self.assertTrue("Deprecated Book" in output) From 5a5d59471719af520ddc203a6b81cd2f6ef81a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20L=C3=B8nne?= Date: Wed, 24 Jul 2013 13:14:32 +0200 Subject: [PATCH 013/117] Fixed typo in Custom management commands documentation. --- docs/howto/custom-management-commands.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 34e68d3700..04ab9bae1b 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -245,7 +245,7 @@ All attributes can be set in your derived class and can be used in Make sure you know what you are doing if you decide to change the value of this option in your custom command if it creates database content that is locale-sensitive and such content shouldn't contain any translations (like - it happens e.g. with django.contrim.auth permissions) as making the locale + it happens e.g. with django.contrib.auth permissions) as making the locale differ from the de facto default 'en-us' might cause unintended effects. See the `Management commands and locales`_ section above for further details. From b2314d9e1e08749f2c05151f9cd44520d2b3a03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 28 Feb 2013 10:00:38 +0200 Subject: [PATCH 014/117] Fixed #19941 -- Modified runtests.py to make running the tests easier. 1. Automatically use tests/../django as the Django version. 2. If settings aren't provided through --settings or DJANGO_SETTINGS_MODULE) then use test_sqlite. --- .../contributing/writing-code/unit-tests.txt | 16 ++++++++++++--- tests/runtests.py | 20 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 7ee65cd6fa..b38c6f6783 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -25,16 +25,26 @@ Quickstart ~~~~~~~~~~ Running the tests requires a Django settings module that defines the -databases to use. To make it easy to get started, Django provides a -sample settings module that uses the SQLite database. To run the tests -with this sample ``settings`` module: +databases to use. To make it easy to get started, Django provides and uses a +sample settings module that uses the SQLite database. To run the tests: .. code-block:: bash git clone git@github.com:django/django.git django-repo cd django-repo/tests + ./runtests.py + +.. versionchanged:: 1.7 + +Older versions of Django required running the tests like this:: + PYTHONPATH=..:$PYTHONPATH python ./runtests.py --settings=test_sqlite +``runtests.py`` now uses the Django package found at ``tests/../django`` (there +isn't a need to add this on your ``PYTHONPATH``) and ``test_sqlite`` for the +settings if settings aren't provided through either ``--settings`` or +:envvar:`DJANGO_SETTINGS_MODULE`. + .. _running-unit-tests-settings: Using another ``settings`` module diff --git a/tests/runtests.py b/tests/runtests.py index 8d5b375fb3..53318a7461 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -7,6 +7,20 @@ import sys import tempfile import warnings +def upath(path): + """ + Separate version of django.utils._os.upath. The django.utils version isn't + usable here, as upath is needed for RUNTESTS_DIR which is needed before + django can be imported. + """ + if sys.version_info[0] != 3 and not isinstance(path, bytes): + fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + return path.decode(fs_encoding) + return path + +RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) +sys.path.insert(0, os.path.dirname(RUNTESTS_DIR)) # 'tests/../' + from django import contrib from django.utils._os import upath from django.utils import six @@ -15,7 +29,6 @@ CONTRIB_MODULE_PATH = 'django.contrib' TEST_TEMPLATE_DIR = 'templates' -RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) CONTRIB_DIR = os.path.dirname(upath(contrib.__file__)) TEMP_DIR = tempfile.mkdtemp(prefix='django_') @@ -331,10 +344,9 @@ if __name__ == "__main__": options, args = parser.parse_args() if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings - elif "DJANGO_SETTINGS_MODULE" not in os.environ: - parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. " - "Set it or use --settings.") else: + if "DJANGO_SETTINGS_MODULE" not in os.environ: + os.environ['DJANGO_SETTINGS_MODULE'] = 'test_sqlite' options.settings = os.environ['DJANGO_SETTINGS_MODULE'] if options.liveserver is not None: From bd0dcc6c89e262780df3c17f18b2462f50b48137 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 18 Jul 2013 11:10:49 -0400 Subject: [PATCH 015/117] Fixed #20766 -- Deprecated FastCGI support. --- django/core/management/commands/runfcgi.py | 8 +++++++- docs/howto/deployment/fastcgi.txt | 3 +++ docs/howto/deployment/index.txt | 8 +++++++- docs/index.txt | 2 +- docs/internals/deprecation.txt | 3 +++ docs/ref/django-admin.txt | 3 +++ docs/topics/install.txt | 9 ++++----- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/django/core/management/commands/runfcgi.py b/django/core/management/commands/runfcgi.py index a60d4ebc59..4e9331fc80 100644 --- a/django/core/management/commands/runfcgi.py +++ b/django/core/management/commands/runfcgi.py @@ -1,3 +1,5 @@ +import warnings + from django.core.management.base import BaseCommand class Command(BaseCommand): @@ -5,6 +7,10 @@ class Command(BaseCommand): args = '[various KEY=val options, use `runfcgi help` for help]' def handle(self, *args, **options): + warnings.warn( + "FastCGI support has been deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning) + from django.conf import settings from django.utils import translation # Activate the current language, because it won't get activated later. @@ -14,7 +20,7 @@ class Command(BaseCommand): pass from django.core.servers.fastcgi import runfastcgi runfastcgi(args) - + def usage(self, subcommand): from django.core.servers.fastcgi import FASTCGI_HELP return FASTCGI_HELP diff --git a/docs/howto/deployment/fastcgi.txt b/docs/howto/deployment/fastcgi.txt index 507e50d1a2..cc8d00966c 100644 --- a/docs/howto/deployment/fastcgi.txt +++ b/docs/howto/deployment/fastcgi.txt @@ -2,6 +2,9 @@ How to use Django with FastCGI, SCGI, or AJP ============================================ +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + .. highlight:: bash Although :doc:`WSGI` is the preferred deployment diff --git a/docs/howto/deployment/index.txt b/docs/howto/deployment/index.txt index ed4bcf3d4a..8b0368ac67 100644 --- a/docs/howto/deployment/index.txt +++ b/docs/howto/deployment/index.txt @@ -10,9 +10,15 @@ ways to easily deploy Django: :maxdepth: 1 wsgi/index - fastcgi checklist +FastCGI support is deprecated and will be removed in Django 1.9. + +.. toctree:: + :maxdepth: 1 + + fastcgi + If you're new to deploying Django and/or Python, we'd recommend you try :doc:`mod_wsgi ` first. In most cases it'll be the easiest, fastest, and most stable deployment choice. diff --git a/docs/index.txt b/docs/index.txt index 7f9d1bd032..8f46db8eb9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -190,7 +190,7 @@ testing of Django applications: * **Deployment:** :doc:`Overview ` | :doc:`WSGI servers ` | - :doc:`FastCGI/SCGI/AJP ` | + :doc:`FastCGI/SCGI/AJP ` (deprecated) | :doc:`Deploying static files ` | :doc:`Tracking code errors by email ` diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7f93e1dc58..f7036d13bd 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -418,6 +418,9 @@ these changes. :ref:`initial SQL data` in ``myapp/models/sql/``. Move your custom SQL files to ``myapp/sql/``. +* FastCGI support via the ``runfcgi`` management command will be + removed. Please deploy your project using WSGI. + 2.0 --- diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index d16766618a..31c6a0f660 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -577,6 +577,9 @@ runfcgi [options] .. django-admin:: runfcgi +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + Starts a set of FastCGI processes suitable for use with any Web server that supports the FastCGI protocol. See the :doc:`FastCGI deployment documentation ` for details. Requires the Python FastCGI module from diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 9cf02d96de..5bcc3f64ec 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -59,11 +59,10 @@ installed. If you can't use mod_wsgi for some reason, fear not: Django supports many other deployment options. One is :doc:`uWSGI `; it works -very well with `nginx`_. Another is :doc:`FastCGI `, -perfect for using Django with servers other than Apache. Additionally, Django -follows the WSGI spec (:pep:`3333`), which allows it to run on a variety of -server platforms. See the `server-arrangements wiki page`_ for specific -installation instructions for each platform. +very well with `nginx`_. Additionally, Django follows the WSGI spec +(:pep:`3333`), which allows it to run on a variety of server platforms. See the +`server-arrangements wiki page`_ for specific installation instructions for +each platform. .. _Apache: http://httpd.apache.org/ .. _nginx: http://nginx.org/ From 10f8a2100279621ca0e0fa47d99ee744741a05e7 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Wed, 24 Jul 2013 14:58:14 -0700 Subject: [PATCH 016/117] Fixed #18168 -- clarified precedence of validation any choices set by formfield_for_choice_field are still subject to model validation of the model field's choices attribute --- docs/ref/contrib/admin/index.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index e5e9428805..137d20a351 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1389,6 +1389,15 @@ templates used by the :class:`ModelAdmin` views: kwargs['choices'] += (('ready', 'Ready for deployment'),) return super(MyModelAdmin, self).formfield_for_choice_field(db_field, request, **kwargs) + .. admonition:: Note + + Any ``choices`` attribute set on the formfield will limited to the form + field only. If the corresponding field on the model has choices set, + the choices provided to the form must be a valid subset of those + choices, otherwise the form submission will fail with + a :exc:`~django.core.exceptions.ValidationError` when the model itself + is validated before saving. + .. method:: ModelAdmin.get_changelist(self, request, **kwargs) Returns the ``Changelist`` class to be used for listing. By default, From dab52d99fc821f31ab64177551b90d0a513f1eee Mon Sep 17 00:00:00 2001 From: Brenton Cleeland Date: Thu, 25 Jul 2013 20:57:49 +1000 Subject: [PATCH 017/117] Fixed #20792 -- Corrected DISALLOWED_USER_AGENTS docs. Thanks simonb for the report. --- docs/ref/middleware.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 4898bab636..d011f054ac 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -37,7 +37,7 @@ defines. See the :doc:`cache documentation `. Adds a few conveniences for perfectionists: * Forbids access to user agents in the :setting:`DISALLOWED_USER_AGENTS` - setting, which should be a list of strings. + setting, which should be a list of compiled regular expression objects. * Performs URL rewriting based on the :setting:`APPEND_SLASH` and :setting:`PREPEND_WWW` settings. From 92476e880caae75b1abad6ca50f821f9698c6c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Thu, 25 Jul 2013 16:25:23 +0300 Subject: [PATCH 018/117] Fixed ._meta.pk_index() virtual field failure --- django/db/models/options.py | 5 +++-- tests/foreign_object/models.py | 3 +++ tests/foreign_object/tests.py | 18 +++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/django/db/models/options.py b/django/db/models/options.py index c6005f379d..b4a3f344ea 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -204,9 +204,10 @@ class Options(object): def pk_index(self): """ - Returns the index of the primary key field in the self.fields list. + Returns the index of the primary key field in the self.concrete_fields + list. """ - return self.fields.index(self.pk) + return self.concrete_fields.index(self.pk) def setup_proxy(self, target): """ diff --git a/tests/foreign_object/models.py b/tests/foreign_object/models.py index eee8091a15..4c58fd15bd 100644 --- a/tests/foreign_object/models.py +++ b/tests/foreign_object/models.py @@ -140,6 +140,9 @@ class Article(models.Model): except ArticleTranslation.DoesNotExist: return '[No translation found]' +class NewsArticle(Article): + pass + class ArticleTranslation(models.Model): article = models.ForeignKey(Article) lang = models.CharField(max_length='2') diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 670fc94dc5..4a7cc3e613 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -1,7 +1,9 @@ import datetime from operator import attrgetter -from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation, ArticleTag, ArticleIdea +from .models import ( + Country, Person, Group, Membership, Friendship, Article, + ArticleTranslation, ArticleTag, ArticleIdea, NewsArticle) from django.test import TestCase from django.utils.translation import activate from django.core.exceptions import FieldError @@ -339,6 +341,20 @@ class MultiColumnFKTests(TestCase): with self.assertRaises(FieldError): Article.objects.filter(ideas__name="idea1") + def test_inheritance(self): + activate("fi") + na = NewsArticle.objects.create(pub_date=datetime.date.today()) + ArticleTranslation.objects.create( + article=na, lang="fi", title="foo", body="bar") + self.assertQuerysetEqual( + NewsArticle.objects.select_related('active_translation'), + [na], lambda x: x + ) + with self.assertNumQueries(1): + self.assertEqual( + NewsArticle.objects.select_related( + 'active_translation')[0].active_translation.title, + "foo") class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs From 8c9240222fd882d9ae3819ff289989df1bcd05d7 Mon Sep 17 00:00:00 2001 From: mark hellewell Date: Thu, 25 Jul 2013 22:48:22 +1000 Subject: [PATCH 019/117] Fixed #18315 -- Documented QueryDict.popitem and QueryDict.pop Thanks gcbirzan for the report. --- docs/ref/request-response.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 060ec02e91..c57a1470d6 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -495,6 +495,26 @@ In addition, ``QueryDict`` has the following methods: >>> q.lists() [(u'a', [u'1', u'2', u'3'])] +.. method:: QueryDict.pop(key) + + Returns a list of values for the given key and removes them from the + dictionary. Raises ``KeyError`` if the key does not exist. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.pop('a') + [u'1', u'2', u'3'] + +.. method:: QueryDict.popitem() + + Removes an arbitrary member of the dictionary (since there's no concept + of ordering), and returns a two value tuple containing the key and a list + of all values for the key. Raises ``KeyError`` when called on an empty + dictionary. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.popitem() + (u'a', [u'1', u'2', u'3']) + .. method:: QueryDict.dict() Returns ``dict`` representation of ``QueryDict``. For every (key, list) From 9b88dd3809a178e5bdc2e4a5d2794c2842a2cd43 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Jul 2013 12:31:53 -0400 Subject: [PATCH 020/117] Fixed #20679 -- Corrected CachedFilesMixin.post_process docstring. Thanks bmispelon for the report. --- django/contrib/staticfiles/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index d085cf723f..cd4ca516d5 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -202,7 +202,7 @@ class CachedFilesMixin(object): def post_process(self, paths, dry_run=False, **options): """ - Post process the given list of files (called from collectstatic). + Post process the given SortedDict of files (called from collectstatic). Processing is actually two separate operations: From bddb4a68181f773f9f8d479b1afb1453439739ba Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Jul 2013 13:03:15 -0400 Subject: [PATCH 021/117] Fixed #20769 -- Added "Python compatibility" section to the 1.6 release notes. --- docs/releases/1.6.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5b3c7903c5..73b48edc85 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -32,6 +32,16 @@ deprecation process for some features`_. .. _`backwards incompatible changes`: `Backwards incompatible changes in 1.6`_ .. _`begun the deprecation process for some features`: `Features deprecated in 1.6`_ +Python compatibility +==================== + +Django 1.6, like Django 1.5, requires Python 2.6.5 or above. Python 3 is also +officially supported. We **highly recommend** the latest minor release for each +supported Python series (2.6.X, 2.7.X, 3.2.X, and 3.3.X). + +Django 1.6 will be the final release series to support Python 2.6; beginning +with Django 1.7, the minimum supported Python version will be 2.7. + What's new in Django 1.6 ======================== From 5ed7ec99b6cef20b941d2f35838864d1e440ac6b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 25 Jul 2013 20:14:18 +0200 Subject: [PATCH 022/117] Added versionadded directive missing from b7bd708. --- docs/ref/class-based-views/base.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 4ad017c342..24058311a4 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -222,6 +222,8 @@ RedirectView .. attribute:: pattern_name + .. versionadded:: 1.6 + The name of the URL pattern to redirect to. Reversing will be done using the same args and kwargs as are passed in for this view. From 8f3aefdec33f6cb4bdda142ff9f7fa423c0bebbd Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 25 Jul 2013 17:17:40 -0500 Subject: [PATCH 023/117] Fixed handling of template loader tests. Previously, the CachedLoaderTests were never run at all. --- .../{loaders.py => test_loaders.py} | 43 +++++++++++-------- tests/template_tests/tests.py | 14 +----- 2 files changed, 27 insertions(+), 30 deletions(-) rename tests/template_tests/{loaders.py => test_loaders.py} (86%) diff --git a/tests/template_tests/loaders.py b/tests/template_tests/test_loaders.py similarity index 86% rename from tests/template_tests/loaders.py rename to tests/template_tests/test_loaders.py index 497b422e7f..6fd17fd04b 100644 --- a/tests/template_tests/loaders.py +++ b/tests/template_tests/test_loaders.py @@ -11,10 +11,15 @@ if __name__ == '__main__': import imp import os.path -import pkg_resources import sys import unittest +try: + import pkg_resources +except ImportError: + pkg_resources = None + + from django.template import TemplateDoesNotExist, Context from django.template.loaders.eggs import Loader as EggLoader from django.template import loader @@ -24,23 +29,6 @@ from django.utils.six import StringIO # Mock classes and objects for pkg_resources functions. -class MockProvider(pkg_resources.NullProvider): - def __init__(self, module): - pkg_resources.NullProvider.__init__(self, module) - self.module = module - - def _has(self, path): - return path in self.module._resources - - def _isdir(self, path): - return False - - def get_resource_stream(self, manager, resource_name): - return self.module._resources[resource_name] - - def _get(self, path): - return self.module._resources[path].read() - class MockLoader(object): pass @@ -57,8 +45,27 @@ def create_egg(name, resources): sys.modules[name] = egg +@unittest.skipUnless(pkg_resources, 'setuptools is not installed') class EggLoaderTest(unittest.TestCase): def setUp(self): + # Defined here b/c at module scope we may not have pkg_resources + class MockProvider(pkg_resources.NullProvider): + def __init__(self, module): + pkg_resources.NullProvider.__init__(self, module) + self.module = module + + def _has(self, path): + return path in self.module._resources + + def _isdir(self, path): + return False + + def get_resource_stream(self, manager, resource_name): + return self.module._resources[resource_name] + + def _get(self, path): + return self.module._resources[path].read() + pkg_resources._provider_factories[MockLoader] = MockProvider self.empty_egg = create_egg("egg_empty", {}) diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 6b8a106623..81f4682f16 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -8,8 +8,7 @@ if __name__ == '__main__': # before importing 'template'. settings.configure() -from datetime import date, datetime, timedelta -import time +from datetime import date, datetime import os import sys import traceback @@ -31,21 +30,12 @@ from django.test.utils import (setup_test_template_loader, from django.utils.encoding import python_2_unicode_compatible from django.utils.formats import date_format from django.utils._os import upath -from django.utils.translation import activate, deactivate, ugettext as _ +from django.utils.translation import activate, deactivate from django.utils.safestring import mark_safe from django.utils import six -from django.utils.tzinfo import LocalTimezone from i18n import TransRealMixin -try: - from .loaders import RenderToStringTest, EggLoaderTest -except ImportError as e: - if "pkg_resources" in e.args[0]: - pass # If setuptools isn't installed, that's fine. Just move on. - else: - raise - # NumPy installed? try: import numpy From 31fadc120213284da76801cc7bc56e9f32d7281b Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Fri, 26 Jul 2013 11:59:40 +0300 Subject: [PATCH 024/117] Fixed #20625 -- Chainable Manager/QuerySet methods. Additionally this patch solves the orthogonal problem that specialized `QuerySet` like `ValuesQuerySet` didn't inherit from the current `QuerySet` type. This wasn't an issue until now because we didn't officially support custom `QuerySet` but it became necessary with the introduction of this new feature. Thanks aaugustin, akaariai, carljm, charettes, mjtamlyn, shaib and timgraham for the reviews. --- django/db/models/__init__.py | 2 +- django/db/models/manager.py | 168 ++++++++++---------------------- django/db/models/query.py | 53 +++++++++- docs/ref/models/querysets.txt | 15 ++- docs/releases/1.7.txt | 7 ++ docs/topics/db/managers.txt | 119 ++++++++++++++++++++++ tests/basic/tests.py | 55 +++++++++++ tests/custom_managers/models.py | 52 ++++++++-- tests/custom_managers/tests.py | 42 ++++++++ tests/queryset_pickle/tests.py | 4 + 10 files changed, 390 insertions(+), 127 deletions(-) diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 33151e068d..2ee525faf1 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -2,7 +2,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp -from django.db.models.query import Q +from django.db.models.query import Q, QuerySet from django.db.models.expressions import F from django.db.models.manager import Manager from django.db.models.base import Model diff --git a/django/db/models/manager.py b/django/db/models/manager.py index b369aedb64..f57944ebbc 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,6 @@ import copy +import inspect + from django.db import router from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models import signals @@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase): ) -class Manager(six.with_metaclass(RenameManagerMethods)): +class BaseManager(six.with_metaclass(RenameManagerMethods)): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 def __init__(self): - super(Manager, self).__init__() + super(BaseManager, self).__init__() self._set_creation_counter() self.model = None self._inherited = False self._db = None + @classmethod + def _get_queryset_methods(cls, queryset_class): + def create_method(name, method): + def manager_method(self, *args, **kwargs): + return getattr(self.get_queryset(), name)(*args, **kwargs) + manager_method.__name__ = method.__name__ + manager_method.__doc__ = method.__doc__ + return manager_method + + new_methods = {} + # Refs http://bugs.python.org/issue1785. + predicate = inspect.isfunction if six.PY3 else inspect.ismethod + for name, method in inspect.getmembers(queryset_class, predicate=predicate): + # Only copy missing methods. + if hasattr(cls, name): + continue + # Only copy public methods or methods with the attribute `queryset_only=False`. + queryset_only = getattr(method, 'queryset_only', None) + if queryset_only or (queryset_only is None and name.startswith('_')): + continue + # Copy the method onto the manager. + new_methods[name] = create_method(name, method) + return new_methods + + @classmethod + def from_queryset(cls, queryset_class, class_name=None): + if class_name is None: + class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) + class_dict = { + '_queryset_class': queryset_class, + } + class_dict.update(cls._get_queryset_methods(queryset_class)) + return type(class_name, (cls,), class_dict) + def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model @@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)): Sets the creation counter value for this instance and increments the class-level copy. """ - self.creation_counter = Manager.creation_counter - Manager.creation_counter += 1 + self.creation_counter = BaseManager.creation_counter + BaseManager.creation_counter += 1 def _copy_to_model(self, model): """ @@ -117,130 +153,30 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def db(self): return self._db or router.db_for_read(self.model) - ####################### - # PROXIES TO QUERYSET # - ####################### - def get_queryset(self): - """Returns a new QuerySet object. Subclasses can override this method - to easily customize the behavior of the Manager. """ - return QuerySet(self.model, using=self._db) - - def none(self): - return self.get_queryset().none() + Returns a new QuerySet object. Subclasses can override this method to + easily customize the behavior of the Manager. + """ + return self._queryset_class(self.model, using=self._db) def all(self): + # We can't proxy this method through the `QuerySet` like we do for the + # rest of the `QuerySet` methods. This is because `QuerySet.all()` + # works by creating a "copy" of the current queryset and in making said + # copy, all the cached `prefetch_related` lookups are lost. See the + # implementation of `RelatedManager.get_queryset()` for a better + # understanding of how this comes into play. return self.get_queryset() - def count(self): - return self.get_queryset().count() - - def dates(self, *args, **kwargs): - return self.get_queryset().dates(*args, **kwargs) - - def datetimes(self, *args, **kwargs): - return self.get_queryset().datetimes(*args, **kwargs) - - def distinct(self, *args, **kwargs): - return self.get_queryset().distinct(*args, **kwargs) - - def extra(self, *args, **kwargs): - return self.get_queryset().extra(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.get_queryset().get(*args, **kwargs) - - def get_or_create(self, **kwargs): - return self.get_queryset().get_or_create(**kwargs) - - def update_or_create(self, **kwargs): - return self.get_queryset().update_or_create(**kwargs) - - def create(self, **kwargs): - return self.get_queryset().create(**kwargs) - - def bulk_create(self, *args, **kwargs): - return self.get_queryset().bulk_create(*args, **kwargs) - - def filter(self, *args, **kwargs): - return self.get_queryset().filter(*args, **kwargs) - - def aggregate(self, *args, **kwargs): - return self.get_queryset().aggregate(*args, **kwargs) - - def annotate(self, *args, **kwargs): - return self.get_queryset().annotate(*args, **kwargs) - - def complex_filter(self, *args, **kwargs): - return self.get_queryset().complex_filter(*args, **kwargs) - - def exclude(self, *args, **kwargs): - return self.get_queryset().exclude(*args, **kwargs) - - def in_bulk(self, *args, **kwargs): - return self.get_queryset().in_bulk(*args, **kwargs) - - def iterator(self, *args, **kwargs): - return self.get_queryset().iterator(*args, **kwargs) - - def earliest(self, *args, **kwargs): - return self.get_queryset().earliest(*args, **kwargs) - - def latest(self, *args, **kwargs): - return self.get_queryset().latest(*args, **kwargs) - - def first(self): - return self.get_queryset().first() - - def last(self): - return self.get_queryset().last() - - def order_by(self, *args, **kwargs): - return self.get_queryset().order_by(*args, **kwargs) - - def select_for_update(self, *args, **kwargs): - return self.get_queryset().select_for_update(*args, **kwargs) - - def select_related(self, *args, **kwargs): - return self.get_queryset().select_related(*args, **kwargs) - - def prefetch_related(self, *args, **kwargs): - return self.get_queryset().prefetch_related(*args, **kwargs) - - def values(self, *args, **kwargs): - return self.get_queryset().values(*args, **kwargs) - - def values_list(self, *args, **kwargs): - return self.get_queryset().values_list(*args, **kwargs) - - def update(self, *args, **kwargs): - return self.get_queryset().update(*args, **kwargs) - - def reverse(self, *args, **kwargs): - return self.get_queryset().reverse(*args, **kwargs) - - def defer(self, *args, **kwargs): - return self.get_queryset().defer(*args, **kwargs) - - def only(self, *args, **kwargs): - return self.get_queryset().only(*args, **kwargs) - - def using(self, *args, **kwargs): - return self.get_queryset().using(*args, **kwargs) - - def exists(self, *args, **kwargs): - return self.get_queryset().exists(*args, **kwargs) - def _insert(self, objs, fields, **kwargs): return insert_query(self.model, objs, fields, **kwargs) - def _update(self, values, **kwargs): - return self.get_queryset()._update(values, **kwargs) - def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) +Manager = BaseManager.from_queryset(QuerySet, class_name='Manager') + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. diff --git a/django/db/models/query.py b/django/db/models/query.py index 087c10de8e..406838f907 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core import exceptions from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields import AutoField +from django.db.models.fields import AutoField, Empty from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) from django.db.models.deletion import Collector @@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20 EmptyResultSet = sql.EmptyResultSet +def _pickle_queryset(class_bases, class_dict): + """ + Used by `__reduce__` to create the initial version of the `QuerySet` class + onto which the output of `__getstate__` will be applied. + + See `__reduce__` for more details. + """ + new = Empty() + new.__class__ = type(class_bases[0].__name__, class_bases, class_dict) + return new + + class QuerySet(object): """ Represents a lazy database lookup for a set of objects. """ + def __init__(self, model=None, query=None, using=None): self.model = model self._db = using @@ -45,6 +58,13 @@ class QuerySet(object): self._prefetch_done = False self._known_related_objects = {} # {rel_field, {pk: rel_obj}} + def as_manager(cls): + # Address the circular dependency between `Queryset` and `Manager`. + from django.db.models.manager import Manager + return Manager.from_queryset(cls)() + as_manager.queryset_only = True + as_manager = classmethod(as_manager) + ######################## # PYTHON MAGIC METHODS # ######################## @@ -70,6 +90,26 @@ class QuerySet(object): obj_dict = self.__dict__.copy() return obj_dict + def __reduce__(self): + """ + Used by pickle to deal with the types that we create dynamically when + specialized queryset such as `ValuesQuerySet` are used in conjunction + with querysets that are *subclasses* of `QuerySet`. + + See `_clone` implementation for more details. + """ + if hasattr(self, '_specialized_queryset_class'): + class_bases = ( + self._specialized_queryset_class, + self._base_queryset_class, + ) + class_dict = { + '_specialized_queryset_class': self._specialized_queryset_class, + '_base_queryset_class': self._base_queryset_class, + } + return _pickle_queryset, (class_bases, class_dict), self.__getstate__() + return super(QuerySet, self).__reduce__() + def __repr__(self): data = list(self[:REPR_OUTPUT_SIZE + 1]) if len(data) > REPR_OUTPUT_SIZE: @@ -528,6 +568,7 @@ class QuerySet(object): # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None delete.alters_data = True + delete.queryset_only = True def _raw_delete(self, using): """ @@ -567,6 +608,7 @@ class QuerySet(object): self._result_cache = None return query.get_compiler(self.db).execute_sql(None) _update.alters_data = True + _update.queryset_only = False def exists(self): if self._result_cache is None: @@ -886,6 +928,15 @@ class QuerySet(object): def _clone(self, klass=None, setup=False, **kwargs): if klass is None: klass = self.__class__ + elif not issubclass(self.__class__, klass): + base_queryset_class = getattr(self, '_base_queryset_class', self.__class__) + class_bases = (klass, base_queryset_class) + class_dict = { + '_base_queryset_class': base_queryset_class, + '_specialized_queryset_class': klass, + } + klass = type(klass.__name__, class_bases, class_dict) + query = self.query.clone() if self._sticky_filter: query.filter_is_sticky = True diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 3963785733..c3f6a660b4 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -121,9 +121,7 @@ described here. QuerySet API ============ -Though you usually won't create one manually — you'll go through a -:class:`~django.db.models.Manager` — here's the formal declaration of a -``QuerySet``: +Here's the formal declaration of a ``QuerySet``: .. class:: QuerySet([model=None, query=None, using=None]) @@ -1866,6 +1864,17 @@ DO_NOTHING do not prevent taking the fast-path in deletion. Note that the queries generated in object deletion is an implementation detail subject to change. +as_manager +~~~~~~~~~~ + +.. classmethod:: as_manager() + +.. versionadded:: 1.7 + +Class method that returns an instance of :class:`~django.db.models.Manager` +with a copy of the ``QuerySet``'s methods. See +:ref:`create-manager-with-queryset-methods` for more details. + .. _field-lookups: Field lookups diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index bec5aaa12a..3526b2bce7 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -30,6 +30,13 @@ security support until the release of Django 1.8. What's new in Django 1.7 ======================== +Calling custom ``QuerySet`` methods from the ``Manager`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`QuerySet.as_manager() ` +class method has been added to :ref:`create Manager with QuerySet methods +`. + Admin shortcuts support time zones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index b940b09d33..3b83865e60 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_. .. _below: manager-types_ +.. _calling-custom-queryset-methods-from-manager: + +Calling custom ``QuerySet`` methods from the ``Manager`` +-------------------------------------------------------- + +While most methods from the standard ``QuerySet`` are accessible directly from +the ``Manager``, this is only the case for the extra methods defined on a +custom ``QuerySet`` if you also implement them on the ``Manager``:: + + class PersonQuerySet(models.QuerySet): + def male(self): + return self.filter(sex='M') + + def female(self): + return self.filter(sex='F') + + class PersonManager(models.Manager): + def get_queryset(self): + return PersonQuerySet() + + def male(self): + return self.get_queryset().male() + + def female(self): + return self.get_queryset().female() + + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female'))) + people = PersonManager() + +This example allows you to call both ``male()`` and ``female()`` directly from +the manager ``Person.people``. + +.. _create-manager-with-queryset-methods: + +Creating ``Manager`` with ``QuerySet`` methods +---------------------------------------------- + +.. versionadded:: 1.7 + +In lieu of the above approach which requires duplicating methods on both the +``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager() +` can be used to create an instance +of ``Manager`` with a copy of a custom ``QuerySet``'s methods:: + + class Person(models.Model): + ... + people = PersonQuerySet.as_manager() + +The ``Manager`` instance created by :meth:`QuerySet.as_manager() +` will be virtually +identical to the ``PersonManager`` from the previous example. + +Not every ``QuerySet`` method makes sense at the ``Manager`` level; for +instance we intentionally prevent the :meth:`QuerySet.delete() +` method from being copied onto +the ``Manager`` class. + +Methods are copied according to the following rules: + +- Public methods are copied by default. +- Private methods (starting with an underscore) are not copied by default. +- Methods with a `queryset_only` attribute set to `False` are always copied. +- Methods with a `queryset_only` attribute set to `True` are never copied. + +For example:: + + class CustomQuerySet(models.QuerySet): + # Available on both Manager and QuerySet. + def public_method(self): + return + + # Available only on QuerySet. + def _private_method(self): + return + + # Available only on QuerySet. + def opted_out_public_method(self): + return + opted_out_public_method.queryset_only = True + + # Available on both Manager and QuerySet. + def _opted_in_private_method(self): + return + _opted_in_private_method.queryset_only = False + +from_queryset +~~~~~~~~~~~~~ + +.. classmethod:: from_queryset(queryset_class) + +For advance usage you might want both a custom ``Manager`` and a custom +``QuerySet``. You can do that by calling ``Manager.from_queryset()`` which +returns a *subclass* of your base ``Manager`` with a copy of the custom +``QuerySet`` methods:: + + class BaseManager(models.Manager): + def __init__(self, *args, **kwargs): + ... + + def manager_only_method(self): + return + + class CustomQuerySet(models.QuerySet): + def manager_and_queryset_method(self): + return + + class MyModel(models.Model): + objects = BaseManager.from_queryset(CustomQueryset)(*args, **kwargs) + +You may also store the generated class into a variable:: + + CustomManager = BaseManager.from_queryset(CustomQueryset) + + class MyModel(models.Model): + objects = CustomManager(*args, **kwargs) + .. _custom-managers-and-inheritance: Custom managers and model inheritance diff --git a/tests/basic/tests.py b/tests/basic/tests.py index fb21b11279..9d4490afc6 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -6,6 +6,7 @@ import threading 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.manager import BaseManager from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six @@ -734,3 +735,57 @@ class ConcurrentSaveTests(TransactionTestCase): t.join() a.save() self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') + + +class ManagerTest(TestCase): + QUERYSET_PROXY_METHODS = [ + 'none', + 'count', + 'dates', + 'datetimes', + 'distinct', + 'extra', + 'get', + 'get_or_create', + 'update_or_create', + 'create', + 'bulk_create', + 'filter', + 'aggregate', + 'annotate', + 'complex_filter', + 'exclude', + 'in_bulk', + 'iterator', + 'earliest', + 'latest', + 'first', + 'last', + 'order_by', + 'select_for_update', + 'select_related', + 'prefetch_related', + 'values', + 'values_list', + 'update', + 'reverse', + 'defer', + 'only', + 'using', + 'exists', + '_update', + ] + + def test_manager_methods(self): + """ + This test ensures that the correct set of methods from `QuerySet` + are copied onto `Manager`. + + It's particularly useful to prevent accidentally leaking new methods + into `Manager`. New `QuerySet` methods that should also be copied onto + `Manager` will need to be added to `ManagerTest.QUERYSET_PROXY_METHODS`. + """ + self.assertEqual( + sorted(BaseManager._get_queryset_methods(QuerySet).keys()), + sorted(self.QUERYSET_PROXY_METHODS), + ) diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index 2f5e62fc7a..44d5eb70da 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -20,6 +20,49 @@ class PersonManager(models.Manager): def get_fun_people(self): return self.filter(fun=True) +# An example of a custom manager that sets get_queryset(). + +class PublishedBookManager(models.Manager): + def get_queryset(self): + return super(PublishedBookManager, self).get_queryset().filter(is_published=True) + +# An example of a custom queryset that copies its methods onto the manager. + +class CustomQuerySet(models.QuerySet): + def filter(self, *args, **kwargs): + queryset = super(CustomQuerySet, self).filter(fun=True) + queryset._filter_CustomQuerySet = True + return queryset + + def public_method(self, *args, **kwargs): + return self.all() + + def _private_method(self, *args, **kwargs): + return self.all() + + def optout_public_method(self, *args, **kwargs): + return self.all() + optout_public_method.queryset_only = True + + def _optin_private_method(self, *args, **kwargs): + return self.all() + _optin_private_method.queryset_only = False + +class BaseCustomManager(models.Manager): + def __init__(self, arg): + super(BaseCustomManager, self).__init__() + self.init_arg = arg + + def filter(self, *args, **kwargs): + queryset = super(BaseCustomManager, self).filter(fun=True) + queryset._filter_CustomManager = True + return queryset + + def manager_only(self): + return self.all() + +CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) + @python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) @@ -27,15 +70,12 @@ class Person(models.Model): fun = models.BooleanField() objects = PersonManager() + custom_queryset_default_manager = CustomQuerySet.as_manager() + custom_queryset_custom_manager = CustomManager('hello') + def __str__(self): return "%s %s" % (self.first_name, self.last_name) -# An example of a custom manager that sets get_queryset(). - -class PublishedBookManager(models.Manager): - def get_queryset(self): - return super(PublishedBookManager, self).get_queryset().filter(is_published=True) - @python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 4fe79fe3fb..7fa58b2a88 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -11,12 +11,54 @@ class CustomManagerTests(TestCase): p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ "Bugs Bunny" ], six.text_type ) + + # Test that the methods of a custom `QuerySet` are properly + # copied onto the default `Manager`. + for manager in ['custom_queryset_default_manager', + 'custom_queryset_custom_manager']: + manager = getattr(Person, manager) + + # Copy public methods. + manager.public_method() + # Don't copy private methods. + with self.assertRaises(AttributeError): + manager._private_method() + # Copy methods with `manager=True` even if they are private. + manager._optin_private_method() + # Don't copy methods with `manager=False` even if they are public. + with self.assertRaises(AttributeError): + manager.optout_public_method() + + # Test that the overriden method is called. + queryset = manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that specialized querysets inherit from our custom queryset. + queryset = manager.values_list('first_name', flat=True).filter() + self.assertEqual(list(queryset), [six.text_type("Bugs")]) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that the custom manager `__init__()` argument has been set. + self.assertEqual(Person.custom_queryset_custom_manager.init_arg, 'hello') + + # Test that the custom manager method is only available on the manager. + Person.custom_queryset_custom_manager.manager_only() + with self.assertRaises(AttributeError): + Person.custom_queryset_custom_manager.all().manager_only() + + # Test that the queryset method doesn't override the custom manager method. + queryset = Person.custom_queryset_custom_manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomManager, True) + # The RelatedManager used on the 'books' descriptor extends the default # manager self.assertIsInstance(p2.books, PublishedBookManager) diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index b4b540c80d..d2f333a9b3 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -90,3 +90,7 @@ class PickleabilityTestCase(TestCase): reloaded = pickle.loads(dumped) self.assertEqual(original, reloaded) self.assertIs(reloaded.__class__, dynclass) + + def test_specialized_queryset(self): + self.assert_pickles(Happening.objects.values('name')) + self.assert_pickles(Happening.objects.values('name').dates('when', 'year')) From 7cca8d56d28e321ffc395c92f82d97adaa0dcf94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anssi=20K=C3=A4=C3=A4ri=C3=A4inen?= Date: Fri, 26 Jul 2013 13:02:32 +0300 Subject: [PATCH 025/117] Fixed related model lookup regression It has been possible to use models of wrong type in related field lookups. For example pigs__in=[a_duck] has worked. Changes to ForeignObject broke that. It might be a good idea to restrict the model types usable in lookups. This should be done intentionally, not accidentally and without any consideration for deprecation path. --- django/db/models/fields/related.py | 2 +- tests/queries/tests.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 11a20983eb..b84693eba4 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1060,7 +1060,7 @@ class ForeignObject(RelatedField): value_list = [] for source in sources: # Account for one-to-one relations when sent a different model - while not isinstance(value, source.model): + while not isinstance(value, source.model) and source.rel: source = source.rel.to._meta.get_field(source.rel.field_name) value_list.append(getattr(value, source.attname)) return tuple(value_list) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 4bacd9a1cc..514b0ee5d7 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -2939,3 +2939,19 @@ class Ticket20788Tests(TestCase): chapter__paragraph__page=page) self.assertQuerysetEqual( sentences_not_in_pub, [book2], lambda x: x) + +class RelatedLookupTypeTests(TestCase): + def test_wrong_type_lookup(self): + oa = ObjectA.objects.create(name="oa") + wrong_type = Order.objects.create(id=oa.pk) + ob = ObjectB.objects.create(name="ob", objecta=oa, num=1) + # Currently Django doesn't care if the object is of correct + # type, it will just use the objecta's related fields attribute + # (id) for model lookup. Making things more restrictive could + # be a good idea... + self.assertQuerysetEqual( + ObjectB.objects.filter(objecta=wrong_type), + [ob], lambda x: x) + self.assertQuerysetEqual( + ObjectB.objects.filter(objecta__in=[wrong_type]), + [ob], lambda x: x) From 2a979d2a7bec485e4b90b7ae99ace0dd16faa948 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 26 Jul 2013 07:22:30 -0400 Subject: [PATCH 026/117] Updated contrib.admin to use Email/URLInputs; refs #16630 --- django/contrib/admin/options.py | 1 + django/contrib/admin/widgets.py | 9 ++++++++- tests/admin_widgets/models.py | 1 + tests/admin_widgets/tests.py | 13 ++++++++----- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index afc7cfc5bd..2c060a05ac 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -69,6 +69,7 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = { models.CharField: {'widget': widgets.AdminTextInputWidget}, models.ImageField: {'widget': widgets.AdminFileWidget}, models.FileField: {'widget': widgets.AdminFileWidget}, + models.EmailField: {'widget': widgets.AdminEmailInputWidget}, } csrf_protect_m = method_decorator(csrf_protect) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 4b79401dbc..eeae846320 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -285,7 +285,14 @@ class AdminTextInputWidget(forms.TextInput): final_attrs.update(attrs) super(AdminTextInputWidget, self).__init__(attrs=final_attrs) -class AdminURLFieldWidget(forms.TextInput): +class AdminEmailInputWidget(forms.EmailInput): + def __init__(self, attrs=None): + final_attrs = {'class': 'vTextField'} + if attrs is not None: + final_attrs.update(attrs) + super(AdminEmailInputWidget, self).__init__(attrs=final_attrs) + +class AdminURLFieldWidget(forms.URLInput): def __init__(self, attrs=None): final_attrs = {'class': 'vURLField'} if attrs is not None: diff --git a/tests/admin_widgets/models.py b/tests/admin_widgets/models.py index ae19d58cc4..2c9bc5b32a 100644 --- a/tests/admin_widgets/models.py +++ b/tests/admin_widgets/models.py @@ -13,6 +13,7 @@ class Member(models.Model): name = models.CharField(max_length=100) birthdate = models.DateTimeField(blank=True, null=True) gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) + email = models.EmailField(blank=True) def __str__(self): return self.name diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 870aa6d455..e8d4e5276b 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -83,6 +83,9 @@ class AdminFormfieldForDBFieldTests(TestCase): def testCharField(self): self.assertFormfield(models.Member, 'name', widgets.AdminTextInputWidget) + def testEmailField(self): + self.assertFormfield(models.Member, 'email', widgets.AdminEmailInputWidget) + def testFileField(self): self.assertFormfield(models.Album, 'cover_art', widgets.AdminFileWidget) @@ -300,29 +303,29 @@ class AdminURLWidgetTest(DjangoTestCase): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', '')), - '' + '' ) self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example.com')), - '

Currently:http://example.com
Change:

' + '

Currently:http://example.com
Change:

' ) def test_render_idn(self): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example-äüö.com')), - '

Currently:http://example-äüö.com
Change:

' + '

Currently:http://example-äüö.com
Change:

' ) def test_render_quoting(self): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example.com/some text')), - '

Currently:http://example.com/<sometag>some text</sometag>
Change:

' + '

Currently:http://example.com/<sometag>some text</sometag>
Change:

' ) self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example-äüö.com/some text')), - '

Currently:http://example-äüö.com/<sometag>some text</sometag>
Change:

' + '

Currently:http://example-äüö.com/<sometag>some text</sometag>
Change:

' ) From 8676318d2dae9a570d2314e4e6da8c00aaf2e2a0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 26 Jul 2013 14:43:46 -0400 Subject: [PATCH 027/117] Fixed #20805 -- Removed an extra colon beside checkboxes in the admin. Thanks CollinAnderson for the report. --- django/contrib/admin/helpers.py | 8 +++++--- django/forms/forms.py | 9 ++++++--- docs/ref/forms/api.txt | 15 ++++++++++++++- docs/releases/1.6.txt | 4 +++- tests/admin_util/tests.py | 4 ++-- tests/forms_tests/tests/test_forms.py | 10 ++++++++++ 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 3ffb85e6c6..b6d5bde932 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -125,14 +125,16 @@ class AdminField(object): contents = conditional_escape(force_text(self.field.label)) if self.is_checkbox: classes.append('vCheckboxLabel') - else: - contents += ':' + if self.field.field.required: classes.append('required') if not self.is_first: classes.append('inline') attrs = {'class': ' '.join(classes)} if classes else {} - return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) + # checkboxes should not have a label suffix as the checkbox appears + # to the left of the label. + return self.field.label_tag(contents=mark_safe(contents), attrs=attrs, + label_suffix='' if self.is_checkbox else None) def errors(self): return mark_safe(self.field.errors.as_ul()) diff --git a/django/forms/forms.py b/django/forms/forms.py index e144eb60f8..ad5daf4416 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -509,20 +509,23 @@ class BoundField(object): ) return self.field.prepare_value(data) - def label_tag(self, contents=None, attrs=None): + def label_tag(self, contents=None, attrs=None, label_suffix=None): """ Wraps the given contents in a