Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

newforms-admin: Merged from trunk up to [7808]. Fixed #7519, #7573

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7809 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 829fd5a9670a581309797f56bfc7e5140550e873 1 parent c349ba4
@brosner brosner authored
Showing with 4,333 additions and 1,979 deletions.
  1. +2 −0  AUTHORS
  2. BIN  django/conf/locale/mk/LC_MESSAGES/django.mo
  3. +3,336 −1,738 django/conf/locale/mk/LC_MESSAGES/django.po
  4. +2 −0  django/contrib/auth/admin.py
  5. +56 −0 django/contrib/auth/fixtures/authtestdata.json
  6. +2 −4 django/contrib/auth/models.py
  7. +3 −3 django/contrib/auth/tests/__init__.py
  8. +22 −1 django/contrib/auth/tests/basic.py
  9. +0 −29 django/contrib/auth/tests/forms.py
  10. +13 −0 django/contrib/auth/urls.py
  11. +1 −1  django/contrib/flatpages/models.py
  12. +1 −5 django/contrib/formtools/tests.py
  13. +2 −2 django/core/management/commands/createcachetable.py
  14. +6 −0 django/core/management/commands/loaddata.py
  15. +6 −7 django/core/management/sql.py
  16. +3 −0  django/core/servers/basehttp.py
  17. +4 −0 django/core/servers/fastcgi.py
  18. +5 −0 django/core/urlresolvers.py
  19. +0 −2  django/db/backends/__init__.py
  20. +1 −2  django/db/backends/mysql/base.py
  21. +1 −2  django/db/backends/mysql_old/base.py
  22. +0 −1  django/db/backends/oracle/base.py
  23. +1 −1  django/db/backends/oracle/creation.py
  24. +1 −1  django/db/backends/postgresql/operations.py
  25. +25 −7 django/db/models/base.py
  26. +7 −13 django/db/models/fields/__init__.py
  27. +24 −11 django/db/models/fields/related.py
  28. +29 −8 django/db/models/options.py
  29. +2 −3 django/db/models/query.py
  30. +17 −0 django/db/models/query_utils.py
  31. +148 −30 django/db/models/sql/query.py
  32. +6 −6 django/db/models/sql/subqueries.py
  33. +11 −15 django/db/models/sql/where.py
  34. +4 −1 django/db/transaction.py
  35. +2 −2 django/middleware/http.py
  36. +7 −3 django/newforms/fields.py
  37. +25 −0 django/test/testcases.py
  38. +5 −4 django/utils/daemonize.py
  39. +5 −3 docs/apache_auth.txt
  40. +25 −0 docs/db-api.txt
  41. +6 −1 docs/flatpages.txt
  42. +1 −1  docs/model-api.txt
  43. +31 −0 docs/testing.txt
  44. 0  tests/regressiontests/extra_regress/__init__.py
  45. +55 −0 tests/regressiontests/extra_regress/models.py
  46. +83 −0 tests/regressiontests/fixtures_regress/fixtures/big-fixture.json
  47. +4 −0 tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json
  48. +44 −4 tests/regressiontests/fixtures_regress/models.py
  49. +1 −1  tests/regressiontests/forms/error_messages.py
  50. +10 −4 tests/regressiontests/forms/fields.py
  51. +40 −0 tests/regressiontests/many_to_one_regress/models.py
  52. +17 −0 tests/regressiontests/model_fields/tests.py
  53. +22 −0 tests/regressiontests/model_inheritance_regress/models.py
  54. 0  tests/regressiontests/model_inheritance_select_related/__init__.py
  55. +47 −0 tests/regressiontests/model_inheritance_select_related/models.py
  56. +77 −63 tests/regressiontests/queries/models.py
  57. 0  tests/regressiontests/select_related_regress/__init__.py
  58. +60 −0 tests/regressiontests/select_related_regress/models.py
  59. +6 −0 tests/regressiontests/string_lookup/models.py
  60. +19 −0 tests/regressiontests/test_client_regress/models.py
View
2  AUTHORS
@@ -95,6 +95,7 @@ answer newbie questions, and generally made Django that much better:
Sengtha Chay <sengtha@e-khmer.com>
ivan.chelubeev@gmail.com
Bryan Chow <bryan at verdjn dot com>
+ Antonis Christofides <anthony@itia.ntua.gr>
Michal Chruszcz <troll@pld-linux.org>
Can Burak Çilingir <canburak@cs.bilgi.edu.tr>
Ian Clelland <clelland@gmail.com>
@@ -195,6 +196,7 @@ answer newbie questions, and generally made Django that much better:
jcrasta@gmail.com
jdetaeye
Zak Johnson <zakj@nox.cx>
+ Nis Jørgensen <nis@superlativ.dk>
Michael Josephson <http://www.sdjournal.com/>
jpellerin@gmail.com
junzhang.jn@gmail.com
View
BIN  django/conf/locale/mk/LC_MESSAGES/django.mo
Binary file not shown
View
5,074 django/conf/locale/mk/LC_MESSAGES/django.po
3,336 additions, 1,738 deletions not shown
View
2  django/contrib/auth/admin.py
@@ -8,6 +8,7 @@
class GroupAdmin(admin.ModelAdmin):
search_fields = ('name',)
+ ordering = ('name',)
filter_horizontal = ('permissions',)
class UserAdmin(admin.ModelAdmin):
@@ -21,6 +22,7 @@ class UserAdmin(admin.ModelAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_filter = ('is_staff', 'is_superuser')
search_fields = ('username', 'first_name', 'last_name', 'email')
+ ordering = ('username',)
filter_horizontal = ('user_permissions',)
def add_view(self, request):
View
56 django/contrib/auth/fixtures/authtestdata.json
@@ -0,0 +1,56 @@
+[
+ {
+ "pk": "1",
+ "model": "auth.user",
+ "fields": {
+ "username": "testclient",
+ "first_name": "Test",
+ "last_name": "Client",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2006-12-17 07:03:31",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+ "email": "testclient@example.com",
+ "date_joined": "2006-12-17 07:03:31"
+ }
+ },
+ {
+ "pk": "2",
+ "model": "auth.user",
+ "fields": {
+ "username": "inactive",
+ "first_name": "Inactive",
+ "last_name": "User",
+ "is_active": false,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2006-12-17 07:03:31",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+ "email": "testclient@example.com",
+ "date_joined": "2006-12-17 07:03:31"
+ }
+ },
+ {
+ "pk": "3",
+ "model": "auth.user",
+ "fields": {
+ "username": "staff",
+ "first_name": "Staff",
+ "last_name": "Member",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": true,
+ "last_login": "2006-12-17 07:03:31",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
+ "email": "staffmember@example.com",
+ "date_joined": "2006-12-17 07:03:31"
+ }
+ }
+]
View
6 django/contrib/auth/models.py
@@ -96,8 +96,7 @@ class Group(models.Model):
class Meta:
verbose_name = _('group')
verbose_name_plural = _('groups')
- ordering = ('name',)
-
+
def __unicode__(self):
return self.name
@@ -150,8 +149,7 @@ class User(models.Model):
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
- ordering = ('username',)
-
+
def __unicode__(self):
return self.username
View
6 django/contrib/auth/tests/__init__.py
@@ -1,8 +1,8 @@
-from django.contrib.auth.tests.basic import BASIC_TESTS
-from django.contrib.auth.tests.forms import FORM_TESTS, PasswordResetFormTestCase
+from django.contrib.auth.tests.basic import BASIC_TESTS, PasswordResetTest
+from django.contrib.auth.tests.forms import FORM_TESTS
__test__ = {
'BASIC_TESTS': BASIC_TESTS,
- 'PASSWORDRESET_TESTS': PasswordResetFormTestCase,
+ 'PASSWORDRESET_TESTS': PasswordResetTest,
'FORM_TESTS': FORM_TESTS,
}
View
23 django/contrib/auth/tests/basic.py
@@ -53,4 +53,25 @@
u'joe@somewhere.org'
>>> u.password
u'!'
-"""
+"""
+
+from django.test import TestCase
+from django.core import mail
+
+class PasswordResetTest(TestCase):
+ fixtures = ['authtestdata.json']
+ urls = 'django.contrib.auth.urls'
+
+ def test_email_not_found(self):
+ "Error is raised if the provided email address isn't currently registered"
+ response = self.client.get('/password_reset/')
+ self.assertEquals(response.status_code, 200)
+ response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
+ self.assertContains(response, "That e-mail address doesn't have an associated user account")
+ self.assertEquals(len(mail.outbox), 0)
+
+ def test_email_found(self):
+ "Email is sent if a valid email address is provided for password reset"
+ response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
+ self.assertEquals(response.status_code, 302)
+ self.assertEquals(len(mail.outbox), 1)
View
29 django/contrib/auth/tests/forms.py
@@ -1,33 +1,4 @@
-from django.core import mail
-from django.test import TestCase
-from django.contrib.auth.models import User
-from django.contrib.auth.forms import PasswordResetForm
-
-class PasswordResetFormTestCase(TestCase):
- def testValidUser(self):
- data = {
- 'email': 'nonexistent@example.com',
- }
- form = PasswordResetForm(data)
- self.assertEqual(form.is_valid(), False)
- self.assertEqual(form["email"].errors, [u"That e-mail address doesn't have an associated user account. Are you sure you've registered?"])
-
- def testEmail(self):
- # TODO: remove my email address from the test ;)
- User.objects.create_user('atestuser', 'atestuser@example.com', 'test789')
- data = {
- 'email': 'atestuser@example.com',
- }
- form = PasswordResetForm(data)
- self.assertEqual(form.is_valid(), True)
- # TODO: look at why using contrib.sites breaks other tests
- form.save(domain_override="example.com")
- self.assertEqual(len(mail.outbox), 1)
- self.assertEqual(mail.outbox[0].subject, u'Password reset on example.com')
- # TODO: test mail body. need to figure out a way to get the password in plain text
- # self.assertEqual(mail.outbox[0].body, '')
-
FORM_TESTS = """
>>> from django.contrib.auth.models import User
>>> from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
View
13 django/contrib/auth/urls.py
@@ -0,0 +1,13 @@
+# These URLs are normally mapped to /admin/urls.py. This URLs file is
+# provided as a convenience to those who want to deploy these URLs elsewhere.
+# This file is also used to provide a reliable view deployment for test purposes.
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ ('^logout/$', 'django.contrib.auth.views.logout'),
+ ('^password_change/$', 'django.contrib.auth.views.password_change'),
+ ('^password_change/done/$', 'django.contrib.auth.views.password_change_done'),
+ ('^password_reset/$', 'django.contrib.auth.views.password_reset')
+)
+
View
2  django/contrib/flatpages/models.py
@@ -8,7 +8,7 @@ class FlatPage(models.Model):
url = models.CharField(_('URL'), max_length=100, validator_list=[validators.isAlphaNumericURL], db_index=True,
help_text=_("Example: '/about/contact/'. Make sure to have leading and trailing slashes."))
title = models.CharField(_('title'), max_length=200)
- content = models.TextField(_('content'))
+ content = models.TextField(_('content'), blank=True)
enable_comments = models.BooleanField(_('enable comments'))
template_name = models.CharField(_('template name'), max_length=70, blank=True,
help_text=_("Example: 'flatpages/contact_page.html'. If this isn't provided, the system will use 'flatpages/default.html'."))
View
6 django/contrib/formtools/tests.py
@@ -21,18 +21,14 @@ class TestForm(forms.Form):
class PreviewTests(TestCase):
+ urls = 'django.contrib.formtools.test_urls'
def setUp(self):
- self._old_root_urlconf = settings.ROOT_URLCONF
- settings.ROOT_URLCONF = 'django.contrib.formtools.test_urls'
# Create a FormPreview instance to share between tests
self.preview = preview.FormPreview(TestForm)
input_template = '<input type="hidden" name="%s" value="%s" />'
self.input = input_template % (self.preview.unused_name('stage'), "%d")
- def tearDown(self):
- settings.ROOT_URLCONF = self._old_root_urlconf
-
def test_unused_name(self):
"""
Verifies name mangling to get uniue field name.
View
4 django/core/management/commands/createcachetable.py
@@ -21,10 +21,10 @@ def handle_label(self, tablename, **options):
for f in fields:
field_output = [qn(f.name), f.db_type()]
field_output.append("%sNULL" % (not f.null and "NOT " or ""))
- if f.unique:
- field_output.append("UNIQUE")
if f.primary_key:
field_output.append("PRIMARY KEY")
+ elif f.unique:
+ field_output.append("UNIQUE")
if f.db_index:
unique = f.unique and "UNIQUE " or ""
index_output.append("CREATE %sINDEX %s_%s ON %s (%s);" % \
View
6 django/core/management/commands/loaddata.py
@@ -162,3 +162,9 @@ def handle(self, *fixture_labels, **options):
else:
if verbosity > 0:
print "Installed %d object(s) from %d fixture(s)" % (object_count, fixture_count)
+
+ # Close the DB connection. This is required as a workaround for an
+ # edge case in MySQL: if the same connection is used to
+ # create tables, load data, and query, the query can return
+ # incorrect results. See Django #7572, MySQL #37735.
+ connection.close()
View
13 django/core/management/sql.py
@@ -268,11 +268,11 @@ def sql_model_create(model, style, known_models=set()):
field_output = [style.SQL_FIELD(qn(f.column)),
style.SQL_COLTYPE(col_type)]
field_output.append(style.SQL_KEYWORD('%sNULL' % (not f.null and 'NOT ' or '')))
- if f.unique and (not f.primary_key or connection.features.allows_unique_and_pk):
- field_output.append(style.SQL_KEYWORD('UNIQUE'))
if f.primary_key:
field_output.append(style.SQL_KEYWORD('PRIMARY KEY'))
- if tablespace and connection.features.supports_tablespaces and (f.unique or f.primary_key) and connection.features.autoindexes_primary_keys:
+ elif f.unique:
+ field_output.append(style.SQL_KEYWORD('UNIQUE'))
+ if tablespace and connection.features.supports_tablespaces and f.unique:
# We must specify the index tablespace inline, because we
# won't be generating a CREATE INDEX statement for this field.
field_output.append(connection.ops.tablespace_sql(tablespace, inline=True))
@@ -355,7 +355,7 @@ def many_to_many_sql_for_model(model, style):
for f in opts.local_many_to_many:
if not isinstance(f.rel, generic.GenericRel):
tablespace = f.db_tablespace or opts.db_tablespace
- if tablespace and connection.features.supports_tablespaces and connection.features.autoindexes_primary_keys:
+ if tablespace and connection.features.supports_tablespaces:
tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace, inline=True)
else:
tablespace_sql = ''
@@ -460,15 +460,14 @@ def sql_indexes_for_model(model, style):
qn = connection.ops.quote_name
for f in model._meta.local_fields:
- if f.db_index and not ((f.primary_key or f.unique) and connection.features.autoindexes_primary_keys):
- unique = f.unique and 'UNIQUE ' or ''
+ if f.db_index and not f.unique:
tablespace = f.db_tablespace or model._meta.db_tablespace
if tablespace and connection.features.supports_tablespaces:
tablespace_sql = ' ' + connection.ops.tablespace_sql(tablespace)
else:
tablespace_sql = ''
output.append(
- style.SQL_KEYWORD('CREATE %sINDEX' % unique) + ' ' + \
+ style.SQL_KEYWORD('CREATE INDEX') + ' ' + \
style.SQL_TABLE(qn('%s_%s' % (model._meta.db_table, f.column))) + ' ' + \
style.SQL_KEYWORD('ON') + ' ' + \
style.SQL_TABLE(qn(model._meta.db_table)) + ' ' + \
View
3  django/core/servers/basehttp.py
@@ -551,6 +551,9 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
from django.conf import settings
self.admin_media_prefix = settings.ADMIN_MEDIA_PREFIX
+ # We set self.path to avoid crashes in log_message() on unsupported
+ # requests (like "OPTIONS").
+ self.path = ''
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
def get_environ(self):
View
4 django/core/servers/fastcgi.py
@@ -40,6 +40,7 @@
workdir=DIRECTORY change to this directory when daemonizing.
outlog=FILE write stdout to this file.
errlog=FILE write stderr to this file.
+ umask=UMASK umask to use when daemonizing (default 022).
Examples:
Run a "standard" fastcgi process on a file-descriptor
@@ -73,6 +74,7 @@
'maxrequests': 0,
'outlog': None,
'errlog': None,
+ 'umask': None,
}
def fastcgi_help(message=None):
@@ -159,6 +161,8 @@ def runfastcgi(argset=[], **kwargs):
daemon_kwargs['out_log'] = options['outlog']
if options['errlog']:
daemon_kwargs['err_log'] = options['errlog']
+ if options['umask']:
+ daemon_kwargs['umask'] = int(options['umask'])
if daemonize:
from django.utils.daemonize import become_daemon
View
5 django/core/urlresolvers.py
@@ -296,3 +296,8 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None):
kwargs = kwargs or {}
return iri_to_uri(u'/' + get_resolver(urlconf).reverse(viewname, *args, **kwargs))
+def clear_url_caches():
+ global _resolver_cache
+ global _callable_cache
+ _resolver_cache.clear()
+ _callable_cache.clear()
View
2  django/db/backends/__init__.py
@@ -41,8 +41,6 @@ def make_debug_cursor(self, cursor):
class BaseDatabaseFeatures(object):
allows_group_by_ordinal = True
- allows_unique_and_pk = True
- autoindexes_primary_keys = True
inline_fk_references = True
needs_datetime_string_cast = True
supports_constraints = True
View
3  django/db/backends/mysql/base.py
@@ -60,7 +60,6 @@
# TRADITIONAL will automatically cause most warnings to be treated as errors.
class DatabaseFeatures(BaseDatabaseFeatures):
- autoindexes_primary_keys = False
inline_fk_references = False
empty_fetchmany_value = ()
update_can_self_select = False
@@ -136,7 +135,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
features = DatabaseFeatures()
ops = DatabaseOperations()
operators = {
- 'exact': '= %s',
+ 'exact': '= BINARY %s',
'iexact': 'LIKE %s',
'contains': 'LIKE BINARY %s',
'icontains': 'LIKE %s',
View
3  django/db/backends/mysql_old/base.py
@@ -64,7 +64,6 @@ def __getattr__(self, attr):
return getattr(self.cursor, attr)
class DatabaseFeatures(BaseDatabaseFeatures):
- autoindexes_primary_keys = False
inline_fk_references = False
empty_fetchmany_value = ()
update_can_self_select = False
@@ -140,7 +139,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
features = DatabaseFeatures()
ops = DatabaseOperations()
operators = {
- 'exact': '= %s',
+ 'exact': '= BINARY %s',
'iexact': 'LIKE %s',
'contains': 'LIKE BINARY %s',
'icontains': 'LIKE %s',
View
1  django/db/backends/oracle/base.py
@@ -24,7 +24,6 @@
class DatabaseFeatures(BaseDatabaseFeatures):
allows_group_by_ordinal = False
- allows_unique_and_pk = False # Suppress UNIQUE/PK for Oracle (ORA-02259)
empty_fetchmany_value = ()
needs_datetime_string_cast = False
supports_tablespaces = True
View
2  django/db/backends/oracle/creation.py
@@ -23,7 +23,7 @@
'ImageField': 'NVARCHAR2(%(max_length)s)',
'IntegerField': 'NUMBER(11)',
'IPAddressField': 'VARCHAR2(15)',
- 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(column)s IS NULL))',
+ 'NullBooleanField': 'NUMBER(1) CHECK ((%(qn_column)s IN (0,1)) OR (%(qn_column)s IS NULL))',
'OneToOneField': 'NUMBER(11)',
'PhoneNumberField': 'VARCHAR2(20)',
'PositiveIntegerField': 'NUMBER(11) CHECK (%(qn_column)s >= 0)',
View
2  django/db/backends/postgresql/operations.py
@@ -97,7 +97,7 @@ def sequence_reset_sql(self, style, model_list):
# Use `coalesce` to set the sequence for each model to the max pk value if there are records,
# or 1 if there are none. Set the `is_called` property (the third argument to `setval`) to true
# if there are records (as the max pk value is already in use), otherwise set it to false.
- for f in model._meta.fields:
+ for f in model._meta.local_fields:
if isinstance(f, models.AutoField):
output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
(style.SQL_KEYWORD('SELECT'),
View
32 django/db/models/base.py
@@ -50,7 +50,15 @@ def __new__(cls, name, bases, attrs):
meta = attr_meta
base_meta = getattr(new_class, '_meta', None)
- new_class.add_to_class('_meta', Options(meta))
+ if getattr(meta, 'app_label', None) is None:
+ # Figure out the app_label by looking one level up.
+ # For 'django.contrib.sites.models', this would be 'sites'.
+ model_module = sys.modules[new_class.__module__]
+ kwargs = {"app_label": model_module.__name__.split('.')[-2]}
+ else:
+ kwargs = {}
+
+ new_class.add_to_class('_meta', Options(meta, **kwargs))
if not abstract:
new_class.add_to_class('DoesNotExist',
subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
@@ -71,11 +79,6 @@ def __new__(cls, name, bases, attrs):
if new_class._default_manager.model._meta.abstract:
old_default_mgr = new_class._default_manager
new_class._default_manager = None
- if getattr(new_class._meta, 'app_label', None) is None:
- # Figure out the app_label by looking one level up.
- # For 'django.contrib.sites.models', this would be 'sites'.
- model_module = sys.modules[new_class.__module__]
- new_class._meta.app_label = model_module.__name__.split('.')[-2]
# Bail out early if we have already created this class.
m = get_model(new_class._meta.app_label, name, False)
@@ -389,6 +392,21 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
for sub_obj in getattr(self, rel_opts_name).all():
sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
+ # Handle any ancestors (for the model-inheritance case). We do this by
+ # traversing to the most remote parent classes -- those with no parents
+ # themselves -- and then adding those instances to the collection. That
+ # will include all the child instances down to "self".
+ parent_stack = self._meta.parents.values()
+ while parent_stack:
+ link = parent_stack.pop()
+ parent_obj = getattr(self, link.name)
+ if parent_obj._meta.parents:
+ parent_stack.extend(parent_obj._meta.parents.values())
+ continue
+ # At this point, parent_obj is base class (no ancestor models). So
+ # delete it and all its descendents.
+ parent_obj._collect_sub_objects(seen_objs)
+
def delete(self):
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." % (self._meta.object_name, self._meta.pk.attname)
@@ -436,7 +454,7 @@ def _get_next_or_previous_in_order(self, is_next):
def _get_FIELD_filename(self, field):
if getattr(self, field.attname): # value is not blank
- return os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname))
+ return os.path.normpath(os.path.join(settings.MEDIA_ROOT, getattr(self, field.attname)))
return ''
def _get_FIELD_url(self, field):
View
20 django/db/models/fields/__init__.py
@@ -85,7 +85,7 @@ def __init__(self, verbose_name=None, name=None, primary_key=False,
self.name = name
self.verbose_name = verbose_name
self.primary_key = primary_key
- self.max_length, self.unique = max_length, unique
+ self.max_length, self._unique = max_length, unique
self.blank, self.null = blank, null
# Oracle treats the empty string ('') as null, so coerce the null
# option whenever '' is a possible value.
@@ -160,6 +160,10 @@ def db_type(self):
except KeyError:
return None
+ def unique(self):
+ return self._unique or self.primary_key
+ unique = property(unique)
+
def validate_full(self, field_data, all_data):
"""
Returns a list of errors for this field. This is the main interface,
@@ -676,7 +680,7 @@ def to_python(self, value):
_("This value must be a decimal number."))
def _format(self, value):
- if isinstance(value, basestring):
+ if isinstance(value, basestring) or value is None:
return value
else:
return self.format_number(value)
@@ -697,8 +701,7 @@ def format_number(self, value):
return u"%.*f" % (self.decimal_places, value)
def get_db_prep_save(self, value):
- if value is not None:
- value = self._format(value)
+ value = self._format(value)
return super(DecimalField, self).get_db_prep_save(value)
def get_db_prep_lookup(self, lookup_type, value):
@@ -1151,12 +1154,3 @@ def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs):
def get_manipulator_field_objs(self):
return [curry(oldforms.XMLLargeTextField, schema_path=self.schema_path)]
-class OrderingField(IntegerField):
- empty_strings_allowed=False
- def __init__(self, with_respect_to, **kwargs):
- self.wrt = with_respect_to
- kwargs['null'] = True
- IntegerField.__init__(self, **kwargs )
-
- def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False, follow=True):
- return [oldforms.HiddenField(name_prefix + self.name)]
View
35 django/db/models/fields/related.py
@@ -185,11 +185,11 @@ def __get__(self, instance, instance_type=None):
def __set__(self, instance, value):
if instance is None:
raise AttributeError, "%s must be accessed via instance" % self.related.opts.object_name
-
- # The similarity of the code below to the code in
+
+ # The similarity of the code below to the code in
# ReverseSingleRelatedObjectDescriptor is annoying, but there's a bunch
# of small differences that would make a common base class convoluted.
-
+
# If null=True, we can assign null here, but otherwise the value needs
# to be an instance of the related class.
if value is None and self.related.field.null == False:
@@ -197,14 +197,14 @@ def __set__(self, instance, value):
(instance._meta.object_name, self.related.get_accessor_name()))
elif value is not None and not isinstance(value, self.related.model):
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
- (value, instance._meta.object_name,
+ (value, instance._meta.object_name,
self.related.get_accessor_name(), self.related.opts.object_name))
-
+
# Set the value of the related field
setattr(value, self.related.field.rel.get_related_field().attname, instance)
# Since we already know what the related object is, seed the related
- # object caches now, too. This avoids another db hit if you get the
+ # object caches now, too. This avoids another db hit if you get the
# object you just set.
setattr(instance, self.cache_name, value)
setattr(value, self.related.field.get_cache_name(), instance)
@@ -243,7 +243,7 @@ def __get__(self, instance, instance_type=None):
def __set__(self, instance, value):
if instance is None:
raise AttributeError, "%s must be accessed via instance" % self._field.name
-
+
# If null=True, we can assign null here, but otherwise the value needs
# to be an instance of the related class.
if value is None and self.field.null == False:
@@ -251,9 +251,9 @@ def __set__(self, instance, value):
(instance._meta.object_name, self.field.name))
elif value is not None and not isinstance(value, self.field.rel.to):
raise ValueError('Cannot assign "%r": "%s.%s" must be a "%s" instance.' %
- (value, instance._meta.object_name,
+ (value, instance._meta.object_name,
self.field.name, self.field.rel.to._meta.object_name))
-
+
# Set the value of the related field
try:
val = getattr(value, self.field.rel.get_related_field().attname)
@@ -262,7 +262,7 @@ def __set__(self, instance, value):
setattr(instance, self.field.attname, val)
# Since we already know what the related object is, seed the related
- # object cache now, too. This avoids another db hit if you get the
+ # object cache now, too. This avoids another db hit if you get the
# object you just set.
setattr(instance, self.field.get_cache_name(), value)
@@ -322,7 +322,9 @@ def clear(self):
clear.alters_data = True
manager = RelatedManager()
- manager.core_filters = {'%s__pk' % rel_field.name: getattr(instance, rel_field.rel.get_related_field().attname)}
+ attname = rel_field.rel.get_related_field().name
+ manager.core_filters = {'%s__%s' % (rel_field.name, attname):
+ getattr(instance, attname)}
manager.model = self.related.model
return manager
@@ -670,6 +672,11 @@ def flatten_data(self, follow, obj=None):
def contribute_to_class(self, cls, name):
super(ForeignKey, self).contribute_to_class(cls, name)
setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
+ if isinstance(self.rel.to, basestring):
+ target = self.rel.to
+ else:
+ target = self.rel.to._meta.db_table
+ cls._meta.duplicate_targets[self.column] = (target, "o2m")
def contribute_to_related_class(self, cls, related):
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
@@ -791,6 +798,12 @@ def contribute_to_class(self, cls, name):
# Set up the accessor for the m2m table name for the relation
self.m2m_db_table = curry(self._get_m2m_db_table, cls._meta)
+ if isinstance(self.rel.to, basestring):
+ target = self.rel.to
+ else:
+ target = self.rel.to._meta.db_table
+ cls._meta.duplicate_targets[self.column] = (target, "m2m")
+
def contribute_to_related_class(self, cls, related):
# m2m relations to self do not have a ManyRelatedObjectsDescriptor,
# as it would be redundant - unless the field is non-symmetrical.
View
37 django/db/models/options.py
@@ -24,7 +24,7 @@
'abstract')
class Options(object):
- def __init__(self, meta):
+ def __init__(self, meta, app_label=None):
self.local_fields, self.local_many_to_many = [], []
self.module_name, self.verbose_name = None, None
self.verbose_name_plural = None
@@ -32,7 +32,7 @@ def __init__(self, meta):
self.ordering = []
self.unique_together = []
self.permissions = []
- self.object_name, self.app_label = None, None
+ self.object_name, self.app_label = None, app_label
self.get_latest_by = None
self.order_with_respect_to = None
self.db_tablespace = settings.DEFAULT_TABLESPACE
@@ -43,8 +43,12 @@ def __init__(self, meta):
self.one_to_one_field = None
self.abstract = False
self.parents = SortedDict()
+ self.duplicate_targets = {}
def contribute_to_class(self, cls, name):
+ from django.db import connection
+ from django.db.backends.util import truncate_name
+
cls._meta = self
self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
# First, construct the default values for these options.
@@ -86,9 +90,13 @@ def contribute_to_class(self, cls, name):
self.verbose_name_plural = string_concat(self.verbose_name, 's')
del self.meta
+ # If the db_table wasn't provided, use the app_label + module_name.
+ if not self.db_table:
+ self.db_table = "%s_%s" % (self.app_label, self.module_name)
+ self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
+
+
def _prepare(self, model):
- from django.db import connection
- from django.db.backends.util import truncate_name
if self.order_with_respect_to:
self.order_with_respect_to = self.get_field(self.order_with_respect_to)
self.ordering = ('_order',)
@@ -107,10 +115,23 @@ def _prepare(self, model):
auto_created=True)
model.add_to_class('id', auto)
- # If the db_table wasn't provided, use the app_label + module_name.
- if not self.db_table:
- self.db_table = "%s_%s" % (self.app_label, self.module_name)
- self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
+ # Determine any sets of fields that are pointing to the same targets
+ # (e.g. two ForeignKeys to the same remote model). The query
+ # construction code needs to know this. At the end of this,
+ # self.duplicate_targets will map each duplicate field column to the
+ # columns it duplicates.
+ collections = {}
+ for column, target in self.duplicate_targets.iteritems():
+ try:
+ collections[target].add(column)
+ except KeyError:
+ collections[target] = set([column])
+ self.duplicate_targets = {}
+ for elt in collections.itervalues():
+ if len(elt) == 1:
+ continue
+ for column in elt:
+ self.duplicate_targets[column] = elt.difference(set([column]))
def add_field(self, field):
# Insert the given field in the order in which it was created, using
View
5 django/db/models/query.py
@@ -3,7 +3,7 @@
from django.conf import settings
from django.db import connection, transaction, IntegrityError
from django.db.models.fields import DateField, FieldDoesNotExist
-from django.db.models.query_utils import Q
+from django.db.models.query_utils import Q, select_related_descend
from django.db.models import signals, sql
from django.dispatch import dispatcher
from django.utils.datastructures import SortedDict
@@ -761,8 +761,7 @@ def get_cached_row(klass, row, index_start, max_depth=0, cur_depth=0,
index_end = index_start + len(klass._meta.fields)
obj = klass(*row[index_start:index_end])
for f in klass._meta.fields:
- if (not f.rel or (not restricted and f.null) or
- (restricted and f.name not in requested) or f.rel.parent_link):
+ if not select_related_descend(f, restricted, requested):
continue
if restricted:
next = requested[f.name]
View
17 django/db/models/query_utils.py
@@ -48,3 +48,20 @@ def __invert__(self):
obj.negate()
return obj
+def select_related_descend(field, restricted, requested):
+ """
+ Returns True if this field should be used to descend deeper for
+ select_related() purposes. Used by both the query construction code
+ (sql.query.fill_related_selections()) and the model instance creation code
+ (query.get_cached_row()).
+ """
+ if not field.rel:
+ return False
+ if field.rel.parent_link:
+ return False
+ if restricted and field.name not in requested:
+ return False
+ if not restricted and field.null:
+ return False
+ return True
+
View
178 django/db/models/sql/query.py
@@ -7,6 +7,7 @@
all about the internals of models in order to get the information it needs.
"""
+import datetime
from copy import deepcopy
from django.utils.tree import Node
@@ -14,9 +15,10 @@
from django.dispatch import dispatcher
from django.db import connection
from django.db.models import signals
+from django.db.models.fields import FieldDoesNotExist
+from django.db.models.query_utils import select_related_descend
from django.db.models.sql.where import WhereNode, EverythingNode, AND, OR
from django.db.models.sql.datastructures import Count
-from django.db.models.fields import FieldDoesNotExist
from django.core.exceptions import FieldError
from datastructures import EmptyResultSet, Empty, MultiJoin
from constants import *
@@ -56,6 +58,7 @@ def __init__(self, model, connection, where=WhereNode):
self.start_meta = None
self.select_fields = []
self.related_select_fields = []
+ self.dupe_avoidance = {}
# SQL-related attributes
self.select = []
@@ -164,6 +167,7 @@ def clone(self, klass=None, **kwargs):
obj.start_meta = self.start_meta
obj.select_fields = self.select_fields[:]
obj.related_select_fields = self.related_select_fields[:]
+ obj.dupe_avoidance = self.dupe_avoidance.copy()
obj.select = self.select[:]
obj.tables = self.tables[:]
obj.where = deepcopy(self.where)
@@ -214,7 +218,7 @@ def get_count(self):
obj.select_related = False
obj.related_select_cols = []
obj.related_select_fields = []
- if obj.distinct and len(obj.select) > 1:
+ if len(obj.select) > 1:
obj = self.clone(CountQuery, _query=obj, where=self.where_class(),
distinct=False)
obj.select = []
@@ -362,10 +366,21 @@ def combine(self, rhs, connector):
item.relabel_aliases(change_map)
self.select.append(item)
self.select_fields = rhs.select_fields[:]
- self.extra_select = rhs.extra_select.copy()
- self.extra_tables = rhs.extra_tables
- self.extra_where = rhs.extra_where
- self.extra_params = rhs.extra_params
+
+ if connector == OR:
+ # It would be nice to be able to handle this, but the queries don't
+ # really make sense (or return consistent value sets). Not worth
+ # the extra complexity when you can write a real query instead.
+ if self.extra_select and rhs.extra_select:
+ raise ValueError("When merging querysets using 'or', you "
+ "cannot have extra(select=...) on both sides.")
+ if self.extra_where and rhs.extra_where:
+ raise ValueError("When merging querysets using 'or', you "
+ "cannot have extra(where=...) on both sides.")
+ self.extra_select.update(rhs.extra_select)
+ self.extra_tables += rhs.extra_tables
+ self.extra_where += rhs.extra_where
+ self.extra_params += rhs.extra_params
# Ordering uses the 'rhs' ordering, unless it has none, in which case
# the current ordering is used.
@@ -439,28 +454,39 @@ def get_columns(self, with_aliases=False):
self._select_aliases = aliases
return result
- def get_default_columns(self, with_aliases=False, col_aliases=None):
+ def get_default_columns(self, with_aliases=False, col_aliases=None,
+ start_alias=None, opts=None, as_pairs=False):
"""
Computes the default columns for selecting every field in the base
model.
Returns a list of strings, quoted appropriately for use in SQL
- directly, as well as a set of aliases used in the select statement.
+ directly, as well as a set of aliases used in the select statement (if
+ 'as_pairs' is True, returns a list of (alias, col_name) pairs instead
+ of strings as the first component and None as the second component).
"""
result = []
- table_alias = self.tables[0]
- root_pk = self.model._meta.pk.column
+ if opts is None:
+ opts = self.model._meta
+ if start_alias:
+ table_alias = start_alias
+ else:
+ table_alias = self.tables[0]
+ root_pk = opts.pk.column
seen = {None: table_alias}
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
aliases = set()
- for field, model in self.model._meta.get_fields_with_model():
+ for field, model in opts.get_fields_with_model():
try:
alias = seen[model]
except KeyError:
alias = self.join((table_alias, model._meta.db_table,
root_pk, model._meta.pk.column))
seen[model] = alias
+ if as_pairs:
+ result.append((alias, field.column))
+ continue
if with_aliases and field.column in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
result.append('%s.%s AS %s' % (qn(alias),
@@ -473,6 +499,8 @@ def get_default_columns(self, with_aliases=False, col_aliases=None):
aliases.add(r)
if with_aliases:
col_aliases.add(field.column)
+ if as_pairs:
+ return result, None
return result, aliases
def get_from_clause(self):
@@ -609,6 +637,11 @@ def find_ordering_name(self, name, opts, alias=None, default_order='ASC',
alias, False)
alias = joins[-1]
col = target.column
+ if not field.rel:
+ # To avoid inadvertent trimming of a necessary alias, use the
+ # refcount to show that we are referencing a non-relation field on
+ # the model.
+ self.ref_alias(alias)
# Must use left outer joins for nullable fields.
for join in joins:
@@ -829,8 +862,8 @@ def join(self, connection, always_create=False, exclusions=(),
if reuse and always_create and table in self.table_map:
# Convert the 'reuse' to case to be "exclude everything but the
- # reusable set for this table".
- exclusions = set(self.table_map[table]).difference(reuse)
+ # reusable set, minus exclusions, for this table".
+ exclusions = set(self.table_map[table]).difference(reuse).union(set(exclusions))
always_create = False
t_ident = (lhs_table, table, lhs_col, col)
if not always_create:
@@ -865,7 +898,8 @@ def join(self, connection, always_create=False, exclusions=(),
return alias
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
- used=None, requested=None, restricted=None, nullable=None):
+ used=None, requested=None, restricted=None, nullable=None,
+ dupe_set=None):
"""
Fill in the information needed for a select_related query. The current
depth is measured as the number of connections away from the root model
@@ -875,6 +909,7 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
if not restricted and self.max_depth and cur_depth > self.max_depth:
# We've recursed far enough; bail out.
return
+
if not opts:
opts = self.get_meta()
root_alias = self.get_initial_alias()
@@ -882,6 +917,10 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
self.related_select_fields = []
if not used:
used = set()
+ if dupe_set is None:
+ dupe_set = set()
+ orig_dupe_set = dupe_set
+ orig_used = used
# Setup for the case when only particular related fields should be
# included in the related selection.
@@ -893,9 +932,10 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
restricted = False
for f, model in opts.get_fields_with_model():
- if (not f.rel or (restricted and f.name not in requested) or
- (not restricted and f.null) or f.rel.parent_link):
+ if not select_related_descend(f, restricted, requested):
continue
+ dupe_set = orig_dupe_set.copy()
+ used = orig_used.copy()
table = f.rel.to._meta.db_table
if nullable or f.null:
promote = True
@@ -906,18 +946,32 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
alias = root_alias
for int_model in opts.get_base_chain(model):
lhs_col = int_opts.parents[int_model].column
+ dedupe = lhs_col in opts.duplicate_targets
+ if dedupe:
+ used.update(self.dupe_avoidance.get(id(opts), lhs_col),
+ ())
+ dupe_set.add((opts, lhs_col))
int_opts = int_model._meta
alias = self.join((alias, int_opts.db_table, lhs_col,
int_opts.pk.column), exclusions=used,
promote=promote)
+ for (dupe_opts, dupe_col) in dupe_set:
+ self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
else:
alias = root_alias
+
+ dedupe = f.column in opts.duplicate_targets
+ if dupe_set or dedupe:
+ used.update(self.dupe_avoidance.get((id(opts), f.column), ()))
+ if dedupe:
+ dupe_set.add((opts, f.column))
+
alias = self.join((alias, table, f.column,
f.rel.get_related_field().column), exclusions=used,
promote=promote)
used.add(alias)
- self.related_select_cols.extend([(alias, f2.column)
- for f2 in f.rel.to._meta.fields])
+ self.related_select_cols.extend(self.get_default_columns(
+ start_alias=alias, opts=f.rel.to._meta, as_pairs=True)[0])
self.related_select_fields.extend(f.rel.to._meta.fields)
if restricted:
next = requested.get(f.name, {})
@@ -927,8 +981,10 @@ def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
new_nullable = f.null
else:
new_nullable = None
+ for dupe_opts, dupe_col in dupe_set:
+ self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
- used, next, restricted, new_nullable)
+ used, next, restricted, new_nullable, dupe_set)
def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
can_reuse=None):
@@ -1048,7 +1104,19 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
# that's harmless.
self.promote_alias(table)
- self.where.add((alias, col, field, lookup_type, value), connector)
+ # To save memory and copying time, convert the value from the Python
+ # object to the actual value used in the SQL query.
+ if field:
+ params = field.get_db_prep_lookup(lookup_type, value)
+ else:
+ params = Field().get_db_prep_lookup(lookup_type, value)
+ if isinstance(value, datetime.datetime):
+ annotation = datetime.datetime
+ else:
+ annotation = bool(value)
+
+ self.where.add((alias, col, field.db_type(), lookup_type, annotation,
+ params), connector)
if negate:
for alias in join_list:
@@ -1058,7 +1126,8 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
for alias in join_list:
if self.alias_map[alias][JOIN_TYPE] == self.LOUTER:
j_col = self.alias_map[alias][RHS_JOIN_COL]
- entry = Node([(alias, j_col, None, 'isnull', True)])
+ entry = Node([(alias, j_col, None, 'isnull', True,
+ [True])])
entry.negate()
self.where.add(entry, AND)
break
@@ -1066,7 +1135,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
# Leaky abstraction artifact: We have to specifically
# exclude the "foo__in=[]" case from this handling, because
# it's short-circuited in the Where class.
- entry = Node([(alias, col, field, 'isnull', True)])
+ entry = Node([(alias, col, None, 'isnull', True, [True])])
entry.negate()
self.where.add(entry, AND)
@@ -1114,7 +1183,9 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
(which gives the table we are joining to), 'alias' is the alias for the
table we are joining to. If dupe_multis is True, any many-to-many or
many-to-one joins will always create a new alias (necessary for
- disjunctive filters).
+ disjunctive filters). If can_reuse is not None, it's a list of aliases
+ that can be reused in these joins (nothing else can be reused in this
+ case).
Returns the final field involved in the join, the target database
column (used for any 'where' constraint), the final 'opts' value and the
@@ -1122,7 +1193,14 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
"""
joins = [alias]
last = [0]
+ dupe_set = set()
+ exclusions = set()
for pos, name in enumerate(names):
+ try:
+ exclusions.add(int_alias)
+ except NameError:
+ pass
+ exclusions.add(alias)
last.append(len(joins))
if name == 'pk':
name = opts.pk.name
@@ -1141,6 +1219,7 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
names = opts.get_all_field_names()
raise FieldError("Cannot resolve keyword %r into field. "
"Choices are: %s" % (name, ", ".join(names)))
+
if not allow_many and (m2m or not direct):
for alias in joins:
self.unref_alias(alias)
@@ -1150,12 +1229,27 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
alias_list = []
for int_model in opts.get_base_chain(model):
lhs_col = opts.parents[int_model].column
+ dedupe = lhs_col in opts.duplicate_targets
+ if dedupe:
+ exclusions.update(self.dupe_avoidance.get(
+ (id(opts), lhs_col), ()))
+ dupe_set.add((opts, lhs_col))
opts = int_model._meta
alias = self.join((alias, opts.db_table, lhs_col,
- opts.pk.column), exclusions=joins)
+ opts.pk.column), exclusions=exclusions)
joins.append(alias)
+ exclusions.add(alias)
+ for (dupe_opts, dupe_col) in dupe_set:
+ self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
cached_data = opts._join_cache.get(name)
orig_opts = opts
+ dupe_col = direct and field.column or field.field.column
+ dedupe = dupe_col in opts.duplicate_targets
+ if dupe_set or dedupe:
+ if dedupe:
+ dupe_set.add((opts, dupe_col))
+ exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col),
+ ()))
if direct:
if m2m:
@@ -1177,9 +1271,11 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
target)
int_alias = self.join((alias, table1, from_col1, to_col1),
- dupe_multis, joins, nullable=True, reuse=can_reuse)
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
alias = self.join((int_alias, table2, from_col2, to_col2),
- dupe_multis, joins, nullable=True, reuse=can_reuse)
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
joins.extend([int_alias, alias])
elif field.rel:
# One-to-one or many-to-one field
@@ -1195,7 +1291,7 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
opts, target)
alias = self.join((alias, table, from_col, to_col),
- exclusions=joins, nullable=field.null)
+ exclusions=exclusions, nullable=field.null)
joins.append(alias)
else:
# Non-relation fields.
@@ -1223,9 +1319,11 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
target)
int_alias = self.join((alias, table1, from_col1, to_col1),
- dupe_multis, joins, nullable=True, reuse=can_reuse)
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
alias = self.join((int_alias, table2, from_col2, to_col2),
- dupe_multis, joins, nullable=True, reuse=can_reuse)
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
joins.extend([int_alias, alias])
else:
# One-to-many field (ForeignKey defined on the target model)
@@ -1243,14 +1341,34 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
opts, target)
alias = self.join((alias, table, from_col, to_col),
- dupe_multis, joins, nullable=True, reuse=can_reuse)
+ dupe_multis, exclusions, nullable=True,
+ reuse=can_reuse)
joins.append(alias)
+ for (dupe_opts, dupe_col) in dupe_set:
+ try:
+ self.update_dupe_avoidance(dupe_opts, dupe_col, int_alias)
+ except NameError:
+ self.update_dupe_avoidance(dupe_opts, dupe_col, alias)
+
if pos != len(names) - 1:
raise FieldError("Join on field %r not permitted." % name)
return field, target, opts, joins, last
+ def update_dupe_avoidance(self, opts, col, alias):
+ """
+ For a column that is one of multiple pointing to the same table, update
+ the internal data structures to note that this alias shouldn't be used
+ for those other columns.
+ """
+ ident = id(opts)
+ for name in opts.duplicate_targets[col]:
+ try:
+ self.dupe_avoidance[ident, name].add(alias)
+ except KeyError:
+ self.dupe_avoidance[ident, name] = set([alias])
+
def split_exclude(self, filter_expr, prefix):
"""
When doing an exclude against any kind of N-to-many relation, we need
View
12 django/db/models/sql/subqueries.py
@@ -49,7 +49,7 @@ def delete_batch_related(self, pk_list):
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = self.where_class()
where.add((None, related.field.m2m_reverse_name(),
- related.field, 'in',
+ related.field.db_type(), 'in', True,
pk_list[offset : offset+GET_ITERATOR_CHUNK_SIZE]),
AND)
self.do_query(related.field.m2m_db_table(), where)
@@ -59,11 +59,11 @@ def delete_batch_related(self, pk_list):
if isinstance(f, generic.GenericRelation):
from django.contrib.contenttypes.models import ContentType
field = f.rel.to._meta.get_field(f.content_type_field_name)
- w1.add((None, field.column, field, 'exact',
- ContentType.objects.get_for_model(cls).id), AND)
+ w1.add((None, field.column, field.db_type(), 'exact', True,
+ [ContentType.objects.get_for_model(cls).id]), AND)
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = self.where_class()
- where.add((None, f.m2m_column_name(), f, 'in',
+ where.add((None, f.m2m_column_name(), f.db_type(), 'in', True,
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
AND)
if w1:
@@ -81,7 +81,7 @@ def delete_batch(self, pk_list):
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
where = self.where_class()
field = self.model._meta.pk
- where.add((None, field.column, field, 'in',
+ where.add((None, field.column, field.db_type(), 'in', True,
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]), AND)
self.do_query(self.model._meta.db_table, where)
@@ -204,7 +204,7 @@ def clear_related(self, related_field, pk_list):
for offset in range(0, len(pk_list), GET_ITERATOR_CHUNK_SIZE):
self.where = self.where_class()
f = self.model._meta.pk
- self.where.add((None, f.column, f, 'in',
+ self.where.add((None, f.column, f.db_type(), 'in', True,
pk_list[offset : offset + GET_ITERATOR_CHUNK_SIZE]),
AND)
self.values = [(related_field.column, None, '%s')]
View
26 django/db/models/sql/where.py
@@ -21,8 +21,9 @@ class WhereNode(tree.Node):
the correct SQL).
The children in this tree are usually either Q-like objects or lists of
- [table_alias, field_name, field_class, lookup_type, value]. However, a
- child could also be any class with as_sql() and relabel_aliases() methods.
+ [table_alias, field_name, db_type, lookup_type, value_annotation,
+ params]. However, a child could also be any class with as_sql() and
+ relabel_aliases() methods.
"""
default = AND
@@ -88,29 +89,24 @@ def as_sql(self, node=None, qn=None):
def make_atom(self, child, qn):
"""
- Turn a tuple (table_alias, field_name, field_class, lookup_type, value)
- into valid SQL.
+ Turn a tuple (table_alias, field_name, db_type, lookup_type,
+ value_annot, params) into valid SQL.
Returns the string for the SQL fragment and the parameters to use for
it.
"""
- table_alias, name, field, lookup_type, value = child
+ table_alias, name, db_type, lookup_type, value_annot, params = child
if table_alias:
lhs = '%s.%s' % (qn(table_alias), qn(name))
else:
lhs = qn(name)
- db_type = field and field.db_type() or None
field_sql = connection.ops.field_cast_sql(db_type) % lhs
- if isinstance(value, datetime.datetime):
+ if value_annot is datetime.datetime:
cast_sql = connection.ops.datetime_cast_sql()
else:
cast_sql = '%s'
- if field:
- params = field.get_db_prep_lookup(lookup_type, value)
- else:
- params = Field().get_db_prep_lookup(lookup_type, value)
if isinstance(params, QueryWrapper):
extra, params = params.data
else:
@@ -123,11 +119,11 @@ def make_atom(self, child, qn):
connection.operators[lookup_type] % cast_sql), params)
if lookup_type == 'in':
- if not value:
+ if not value_annot:
raise EmptyResultSet
if extra:
return ('%s IN %s' % (field_sql, extra), params)
- return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value))),
+ return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(params))),
params)
elif lookup_type in ('range', 'year'):
return ('%s BETWEEN %%s and %%s' % field_sql, params)
@@ -135,8 +131,8 @@ def make_atom(self, child, qn):
return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type,
field_sql), params)
elif lookup_type == 'isnull':
- return ('%s IS %sNULL' % (field_sql, (not value and 'NOT ' or '')),
- params)
+ return ('%s IS %sNULL' % (field_sql,
+ (not value_annot and 'NOT ' or '')), ())
elif lookup_type == 'search':
return (connection.ops.fulltext_search_sql(field_sql), params)
elif lookup_type in ('regex', 'iregex'):
View
5 django/db/transaction.py
@@ -196,7 +196,10 @@ def _commit_on_success(*args, **kw):
managed(True)
try:
res = func(*args, **kw)
- except Exception, e:
+ except (Exception, KeyboardInterrupt, SystemExit):
+ # (We handle KeyboardInterrupt and SystemExit specially, since
+ # they don't inherit from Exception in Python 2.5, but we
+ # should treat them uniformly here.)
if is_dirty():
rollback()
raise
View
4 django/middleware/http.py
@@ -19,14 +19,14 @@ def process_response(self, request, response):
# Setting the status is enough here. The response handling path
# automatically removes content for this status code (in
# http.conditional_content_removal()).
- response.status = 304
+ response.status_code = 304
if response.has_header('Last-Modified'):
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None)
if if_modified_since == response['Last-Modified']:
# Setting the status code is enough here (same reasons as
# above).
- response.status = 304
+ response.status_code = 304
return response
View
10 django/newforms/fields.py
@@ -535,13 +535,17 @@ class BooleanField(Field):
def clean(self, value):
"""Returns a Python boolean object."""
- super(BooleanField, self).clean(value)
# Explicitly check for the string 'False', which is what a hidden field
# will submit for False. Because bool("True") == True, we don't need to
# handle that explicitly.
if value == 'False':
- return False
- return bool(value)
+ value = False
+ else:
+ value = bool(value)
+ super(BooleanField, self).clean(value)
+ if not value and self.required:
+ raise ValidationError(self.error_messages['required'])
+ return value
class NullBooleanField(BooleanField):
"""
View
25 django/test/testcases.py
@@ -4,10 +4,12 @@
from django.http import QueryDict
from django.db import transaction
+from django.conf import settings
from django.core import mail
from django.core.management import call_command
from django.test import _doctest as doctest
from django.test.client import Client
+from django.core.urlresolvers import clear_url_caches
normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
@@ -54,6 +56,8 @@ def _pre_setup(self):
* Flushing the database.
* If the Test Case class has a 'fixtures' member, installing the
named fixtures.
+ * If the Test Case class has a 'urls' member, replace the
+ ROOT_URLCONF with it.
* Clearing the mail test outbox.
"""
call_command('flush', verbosity=0, interactive=False)
@@ -61,6 +65,10 @@ def _pre_setup(self):
# We have to use this slightly awkward syntax due to the fact
# that we're using *args and **kwargs together.
call_command('loaddata', *self.fixtures, **{'verbosity': 0})
+ if hasattr(self, 'urls'):
+ self._old_root_urlconf = settings.ROOT_URLCONF
+ settings.ROOT_URLCONF = self.urls
+ clear_url_caches()
mail.outbox = []
def __call__(self, result=None):
@@ -79,6 +87,23 @@ def __call__(self, result=None):
result.addError(self, sys.exc_info())
return
super(TestCase, self).__call__(result)
+ try:
+ self._post_teardown()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except Exception:
+ import sys
+ result.addError(self, sys.exc_info())
+ return
+
+ def _post_teardown(self):
+ """ Performs any post-test things. This includes:
+
+ * Putting back the original ROOT_URLCONF if it was changed.
+ """
+ if hasattr(self, '_old_root_urlconf'):
+ settings.ROOT_URLCONF = self._old_root_urlconf
+ clear_url_caches()
def assertRedirects(self, response, expected_url, status_code=302,
target_status_code=200, host=None):
View
9 django/utils/daemonize.py
@@ -2,7 +2,8 @@
import sys
if os.name == 'posix':
- def become_daemon(our_home_dir='.', out_log='/dev/null', err_log='/dev/null'):
+ def become_daemon(our_home_dir='.', out_log='/dev/null',
+ err_log='/dev/null', umask=022):
"Robustly turn into a UNIX daemon, running in our_home_dir."
# First fork
try:
@@ -13,7 +14,7 @@ def become_daemon(our_home_dir='.', out_log='/dev/null', err_log='/dev/null'):
sys.exit(1)
os.setsid()
os.chdir(our_home_dir)
- os.umask(0)
+ os.umask(umask)
# Second fork
try:
@@ -32,13 +33,13 @@ def become_daemon(our_home_dir='.', out_log='/dev/null', err_log='/dev/null'):
# Set custom file descriptors so that they get proper buffering.
sys.stdout, sys.stderr = so, se
else:
- def become_daemon(our_home_dir='.', out_log=None, err_log=None):
+ def become_daemon(our_home_dir='.', out_log=None, err_log=None, umask=022):
"""
If we're not running under a POSIX system, just simulate the daemon
mode by doing redirections and directory changing.
"""
os.chdir(our_home_dir)
- os.umask(0)
+ os.umask(umask)
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
View
8 docs/apache_auth.txt
@@ -39,9 +39,10 @@ with the standard ``Auth*`` and ``Require`` directives::
example at the bottom of this note).
You'll also need to insert configuration directives that prevent Apache
- from trying to use other authentication modules. Depending on which other
- authentication modules you have loaded, you might need one or more of
- the following directives::
+ from trying to use other authentication modules, as well as specifying
+ the ``AuthUserFile`` directive and pointing it to ``/dev/null``. Depending
+ on which other authentication modules you have loaded, you might need one
+ or more of the following directives::
AuthBasicAuthoritative Off
AuthDefaultAuthoritative Off
@@ -65,6 +66,7 @@ with the standard ``Auth*`` and ``Require`` directives::
<Location /example/>
AuthType Basic
AuthName "example.com"
+ **AuthUserFile /dev/null**
**AuthBasicAuthoritative Off**
Require valid-user
View
25 docs/db-api.txt
@@ -443,6 +443,31 @@ This is roughly equivalent to::
Note, however, that the first of these will raise ``IndexError`` while the
second will raise ``DoesNotExist`` if no objects match the given criteria.
+Combining QuerySets
+-------------------
+
+If you have two ``QuerySet`` instances that act on the same model, you can
+combine them using ``&`` and ``|`` to get the items that are in both result
+sets or in either results set, respectively. For example::
+
+ Entry.objects.filter(pubdate__gte=date1) & \
+ Entry.objects.filter(headline__startswith="What")
+
+will combine the two queries into a single SQL query. Of course, in this case
+you could have achieved the same result using multiple filters on the same
+``QuerySet``, but sometimes the ability to combine individual ``QuerySet``
+instance is useful.
+
+Be careful, if you are using ``extra()`` to add custom handling to your
+``QuerySet`` however. All the ``extra()`` components are merged and the result
+may or may not make sense. If you are using custom SQL fragments in your
+``extra()`` calls, Django will not inspect these fragments to see if they need
+to be rewritten because of changes in the merged query. So test the effects
+carefully. Also realise that if you are combining two ``QuerySets`` with
+``|``, you cannot use ``extra(select=...)`` or ``extra(where=...)`` on *both*
+``QuerySets``. You can only use those calls on one or the other (Django will
+raise a ``ValueError`` if you try to use this incorrectly).
+
QuerySet methods that return new QuerySets
------------------------------------------
View
7 docs/flatpages.txt
@@ -14,9 +14,14 @@ custom Django application.
A flatpage can use a custom template or a default, systemwide flatpage
template. It can be associated with one, or multiple, sites.
+**New in Django development version**
+
+The content field may optionally be left blank if you prefer to put your
+content in a custom template.
+
Here are some examples of flatpages on Django-powered sites:
- * http://www.chicagocrime.org/about/
+ * http://www.everyblock.com/about/
* http://www.lawrence.com/about/contact/
Installation
View
2  docs/model-api.txt
@@ -1677,7 +1677,7 @@ still only creating one database table per child model at the database level.
When an abstract base class is created, Django makes any ``Meta`` inner class
you declared on the base class available as an attribute. If a child class
-does not declared its own ``Meta`` class, it will inherit the parent's
+does not declare its own ``Meta`` class, it will inherit the parent's
``Meta``. If the child wants to extend the parent's ``Meta`` class, it can
subclass it. For example::
View
31 docs/testing.txt
@@ -797,6 +797,37 @@ another test, or by the order of test execution.
.. _dumpdata documentation: ../django-admin/#dumpdata-appname-appname
.. _loaddata documentation: ../django-admin/#loaddata-fixture-fixture
+URLconf configuration
+~~~~~~~~~~~~~~~~~~~~~
+
+**New in Django development version**
+
+If your application provides views, you may want to include tests that
+use the test client to exercise those views. However, an end user is free
+to deploy the views in your application at any URL of their choosing.
+This means that your tests can't rely upon the fact that your views will
+be available at a particular URL.
+
+In order to provide a reliable URL space for your test,
+``django.test.TestCase`` provides the ability to customize the URLconf
+configuration for the duration of the execution of a test suite.
+If your ``TestCase`` instance defines an ``urls`` attribute, the
+``TestCase`` will use the value of that attribute as the ``ROOT_URLCONF``
+for the duration of that test.
+
+For example::
+
+ from django.test import TestCase
+
+ class TestMyViews(TestCase):
+ urls = 'myapp.test_urls'
+
+ def testIndexPageView(self):
+ # Here you'd test your view using ``Client``.
+
+This test case will use the contents of ``myapp.test_urls`` as the
+URLconf for the duration of the test case.
+
Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
View
0  tests/regressiontests/extra_regress/__init__.py
No changes.
View
55 tests/regressiontests/extra_regress/models.py
@@ -0,0 +1,55 @@
+import copy
+
+from django.db import models
+from django.db.models.query import Q
+
+
+class RevisionableModel(models.Model):
+ base = models.ForeignKey('self', null=True)
+ title = models.CharField(blank=True, max_length=255)
+
+ def __unicode__(self):
+ return u"%s (%s, %s)" % (self.title, self.id, self.base.id)
+
+ def save(self):
+ super(RevisionableModel, self).save()
+ if not self.base:
+ self.base = self
+ super(RevisionableModel, self).save()
+
+ def new_revision(self):
+ new_revision = copy.copy(self)
+ new_revision.pk = None
+ return new_revision
+
+__test__ = {"API_TESTS": """
+### Regression tests for #7314 and #7372
+
+>>> rm = RevisionableModel.objects.create(title='First Revision')
+>>> rm.pk, rm.base.pk
+(1, 1)
+
+>>> rm2 = rm.new_revision()
+>>> rm2.title = "Second Revision"
+>>> rm2.save()
+>>> print u"%s of %s" % (rm2.title, rm2.base.title)
+Second Revision of First Revision
+
+>>> rm2.pk, rm2.base.pk
+(2, 1)
+
+Queryset to match most recent revision:
+>>> qs = RevisionableModel.objects.extra(where=["%(table)s.id IN (SELECT MAX(rev.id) FROM %(table)s AS rev GROUP BY rev.base_id)" % {'table': RevisionableModel._meta.db_table,}],)
+>>> qs
+[<RevisionableModel: Second Revision (2, 1)>]
+
+Queryset to search for string in title:
+>>> qs2 = RevisionableModel.objects.filter(title__contains="Revision")
+>>> qs2
+[<RevisionableModel: First Revision (1, 1)>, <RevisionableModel: Second Revision (2, 1)>]
+
+Following queryset should return the most recent revision:
+>>> qs & qs2
+[<RevisionableModel: Second Revision (2, 1)>]
+
+"""}
View
83 tests/regressiontests/fixtures_regress/fixtures/big-fixture.json
@@ -0,0 +1,83 @@
+[
+ {
+ "pk": 6,
+ "model": "fixtures_regress.channel",
+ "fields": {
+ "name": "Business"
+ }
+ },
+
+ {
+ "pk": 1,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 1",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 2,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 2",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 3,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 3",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 4,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 4",
+ "channels": [6]
+ }
+ },
+
+ {
+ "pk": 5,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 5",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 6,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 6",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 7,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 7",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 8,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Article Title 8",
+ "channels": [6]
+ }
+ },
+ {
+ "pk": 9,
+ "model": "fixtures_regress.article",
+ "fields": {
+ "title": "Yet Another Article",
+ "channels": [6]
+ }
+ }
+]
View
4 tests/regressiontests/fixtures_regress/fixtures/model-inheritance.json
@@ -0,0 +1,4 @@
+[
+ {"pk": 1, "model": "fixtures_regress.parent", "fields": {"name": "fred"}},
+ {"pk": 1, "model": "fixtures_regress.child", "fields": {"data": "apple"}}
+]
View
48 tests/regressiontests/fixtures_regress/models.py
@@ -20,7 +20,7 @@ class Meta:
class Stuff(models.Model):
name = models.CharField(max_length=20, null=True)
owner = models.ForeignKey(User, null=True)
-
+
def __unicode__(self):
# Oracle doesn't distinguish between None and the empty string.
# This hack makes the test case pass using Oracle.
@@ -38,13 +38,29 @@ def __init__(self, *args, **kwargs):
super(Absolute, self).__init__(*args, **kwargs)
Absolute.load_count += 1
+class Parent(models.Model):
+ name = models.CharField(max_length=10)
+
+class Child(Parent):
+ data = models.CharField(max_length=10)
+
+# Models to regresison check #7572
+class Channel(models.Model):
+ name = models.CharField(max_length=255)
+
+class Article(models.Model):
+ title = models.CharField(max_length=255)
+ channels = models.ManyToManyField(Channel)
+
+ class Meta:
+ ordering = ('id',)
__test__ = {'API_TESTS':"""
>>> from django.core import management
# Load a fixture that uses PK=1
>>> management.call_command('loaddata', 'sequence', verbosity=0)
-
+
# Create a new animal. Without a sequence reset, this new object
# will take a PK of 1 (on Postgres), and the save will fail.
# This is a regression test for ticket #3790.
@@ -61,9 +77,9 @@ def __init__(self, *args, **kwargs):
[<Stuff: None is owned by None>]
###############################################
-# Regression test for ticket #6436 --
+# Regression test for ticket #6436 --
# os.path.join will throw away the initial parts of a path if it encounters
-# an absolute path. This means that if a fixture is specified as an absolute path,
+# an absolute path. This means that if a fixture is specified as an absolute path,
# we need to make sure we don't discover the absolute path in every fixture directory.
>>> load_absolute_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'absolute.json')
@@ -94,4 +110,28 @@ def __init__(self, *args, **kwargs):
>>> sys.stderr = savestderr
+###############################################
+# Test for ticket #7565 -- PostgreSQL sequence resetting checks shouldn't
+# ascend to parent models when inheritance is used (since they are treated
+# individually).
+
+>>> management.call_command('loaddata', 'model-inheritance.json', verbosity=0)
+
+###############################################
+# Test for ticket #7572 -- MySQL has a problem if the same connection is
+# used to create tables, load data, and then query over that data.
+# To compensate, we close the connection after running loaddata.
+# This ensures that a new connection is opened when test queries are issued.
+
+>>> management.call_command('loaddata', 'big-fixture.json', verbosity=0)
+
+>>> articles = Article.objects.exclude(id=9)
+>>> articles.values_list('id', flat=True)
+[1, 2, 3, 4, 5, 6, 7, 8]
+
+# Just for good measure, run the same query again. Under the influence of
+# ticket #7572, this will give a different result to the previous call.
+>>> articles.values_list('id', flat=True)
+[1, 2, 3, 4, 5, 6, 7, 8]
+
"""}
View
2  tests/regressiontests/forms/error_messages.py
@@ -237,7 +237,7 @@
Traceback (most recent call last):
...
ValidationError: [u'INVALID']
->>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com')
+>>> f.clean('http://www.broken.djangoproject.com')
Traceback (most recent call last):
...
ValidationError: [u'INVALID LINK']
View
14 tests/regressiontests/forms/fields.py
@@ -887,7 +887,7 @@
Traceback (most recent call last):
...
ValidationError: [u'Enter a valid URL.']
->>> f.clean('http://www.jfoiwjfoi23jfoijoaijfoiwjofiwjefewl.com') # bad domain
+>>> f.clean('http://www.broken.djangoproject.com') # bad domain
Traceback (most recent call last):
...
ValidationError: [u'This URL appears to be a broken link.']
@@ -937,18 +937,24 @@
>>> f.clean(True)
True
>>> f.clean(False)
-False
+Traceback (most recent call last):
+...