summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2013-06-19 12:03:20 +0100
committerAndrew Godwin <andrew@aeracode.org>2013-06-19 12:05:23 +0100
commit9daf81b94e7945d7adfdf62206cf9cb436166f0d (patch)
tree0ca2a77977432d0c139886297afb433569b588ca /django
parent315ab41e416c777d4f42932d42df07872e8f8895 (diff)
parentd9a43545be1af95a13c181c8b178f5631d3a4148 (diff)
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/db/models/loading.py
Diffstat (limited to 'django')
-rw-r--r--django/contrib/admin/forms.py9
-rw-r--r--django/contrib/admin/options.py117
-rw-r--r--django/contrib/admin/templates/admin/change_form.html8
-rw-r--r--django/contrib/admin/templates/admin/change_list.html8
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html3
-rw-r--r--django/contrib/admin/templates/admin/delete_selected_confirmation.html3
-rw-r--r--django/contrib/admin/templates/admin/object_history.html3
-rw-r--r--django/contrib/admin/templates/admin/submit_line.html5
-rw-r--r--django/contrib/admin/templatetags/admin_list.py2
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py3
-rw-r--r--django/contrib/admin/templatetags/admin_urls.py42
-rw-r--r--django/contrib/admin/tests.py9
-rw-r--r--django/contrib/admin/views/main.py1
-rw-r--r--django/contrib/auth/forms.py39
-rw-r--r--django/contrib/auth/hashers.py37
-rw-r--r--django/contrib/auth/management/__init__.py15
-rw-r--r--django/contrib/auth/models.py2
-rw-r--r--django/contrib/auth/tests/test_custom_user.py12
-rw-r--r--django/contrib/auth/tests/test_handlers.py8
-rw-r--r--django/contrib/auth/tests/test_hashers.py84
-rw-r--r--django/contrib/auth/tests/test_management.py7
-rw-r--r--django/contrib/auth/tests/test_models.py2
-rw-r--r--django/contrib/contenttypes/management.py7
-rw-r--r--django/contrib/flatpages/forms.py16
-rw-r--r--django/contrib/formtools/wizard/views.py5
-rw-r--r--django/contrib/gis/forms/fields.py6
-rw-r--r--django/contrib/sites/models.py4
-rw-r--r--django/core/compat_checks/__init__.py0
-rw-r--r--django/core/compat_checks/base.py39
-rw-r--r--django/core/compat_checks/django_1_6_0.py37
-rw-r--r--django/core/exceptions.py64
-rw-r--r--django/core/management/commands/checksetup.py14
-rw-r--r--django/core/management/commands/flush.py33
-rw-r--r--django/core/management/commands/runserver.py5
-rw-r--r--django/core/management/sql.py4
-rw-r--r--django/core/management/validation.py4
-rw-r--r--django/core/urlresolvers.py5
-rw-r--r--django/core/validators.py8
-rw-r--r--django/db/backends/__init__.py8
-rw-r--r--django/db/backends/mysql/base.py7
-rw-r--r--django/db/backends/oracle/base.py12
-rw-r--r--django/db/backends/postgresql_psycopg2/operations.py19
-rw-r--r--django/db/backends/sqlite3/base.py12
-rw-r--r--django/db/models/__init__.py2
-rw-r--r--django/db/models/base.py43
-rw-r--r--django/db/models/fields/__init__.py142
-rw-r--r--django/db/models/fields/related.py7
-rw-r--r--django/db/models/loading.py75
-rw-r--r--django/db/models/sql/compiler.py64
-rw-r--r--django/db/models/sql/query.py29
-rw-r--r--django/forms/fields.py111
-rw-r--r--django/forms/forms.py8
-rw-r--r--django/forms/formsets.py16
-rw-r--r--django/forms/models.py102
-rw-r--r--django/forms/util.py13
-rw-r--r--django/http/response.py4
-rw-r--r--django/test/client.py2
-rw-r--r--django/test/testcases.py73
-rw-r--r--django/utils/ipv6.py5
-rw-r--r--django/views/generic/base.py20
-rw-r--r--django/views/generic/edit.py4
61 files changed, 1038 insertions, 410 deletions
diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py
index 38c445f71a..6814fc9083 100644
--- a/django/contrib/admin/forms.py
+++ b/django/contrib/admin/forms.py
@@ -22,15 +22,12 @@ class AdminAuthenticationForm(AuthenticationForm):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
message = ERROR_MESSAGE
+ params = {'username': self.username_field.verbose_name}
if username and password:
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
elif not self.user_cache.is_active or not self.user_cache.is_staff:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
return self.cleaned_data
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 34583ebf74..942385e1c6 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -13,6 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o
model_format_dict, NestedObjects, lookup_needs_distinct)
from django.contrib.admin import validation
from django.contrib.admin.templatetags.admin_static import static
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError, FieldError
@@ -33,6 +34,7 @@ from django.utils.html import escape, escapejs
from django.utils.safestring import mark_safe
from django.utils import six
from django.utils.deprecation import RenameMethodsBase
+from django.utils.http import urlencode
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
@@ -393,6 +395,7 @@ class ModelAdmin(BaseModelAdmin):
save_as = False
save_on_top = False
paginator = Paginator
+ preserve_filters = True
inlines = []
# Custom templates (designed to be over-ridden in subclasses)
@@ -755,6 +758,27 @@ class ModelAdmin(BaseModelAdmin):
"""
return self.list_filter
+ def get_preserved_filters(self, request):
+ """
+ Returns the preserved filters querystring.
+ """
+
+ # FIXME: We can remove that getattr as soon as #20619 is fixed.
+ match = getattr(request, 'resolver_match', None)
+
+ if self.preserve_filters and match:
+ opts = self.model._meta
+ current_url = '%s:%s' % (match.namespace, match.url_name)
+ changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
+ if current_url == changelist_url:
+ preserved_filters = request.GET.urlencode()
+ else:
+ preserved_filters = request.GET.get('_changelist_filters')
+
+ if preserved_filters:
+ return urlencode({'_changelist_filters': preserved_filters})
+ return ''
+
def construct_change_message(self, request, form, formsets):
"""
Construct a change message from a changed object.
@@ -846,6 +870,8 @@ class ModelAdmin(BaseModelAdmin):
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta
app_label = opts.app_label
+ preserved_filters = self.get_preserved_filters(request)
+ form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
context.update({
'add': add,
'change': change,
@@ -877,11 +903,19 @@ class ModelAdmin(BaseModelAdmin):
"""
opts = obj._meta
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
# Here, we distinguish between different save types by checking for
# the presence of keys in request.POST.
- if "_continue" in request.POST:
+ if "_popup" in request.POST:
+ return HttpResponse(
+ '<!DOCTYPE html><html><head><title></title></head><body>'
+ '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
+ # escape() calls force_text.
+ (escape(pk_value), escapejs(obj)))
+
+ elif "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
if post_url_continue is None:
@@ -889,20 +923,16 @@ class ModelAdmin(BaseModelAdmin):
(opts.app_label, opts.model_name),
args=(pk_value,),
current_app=self.admin_site.name)
- if "_popup" in request.POST:
- post_url_continue += "?_popup=1"
+ post_url_continue = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue)
return HttpResponseRedirect(post_url_continue)
- if "_popup" in request.POST:
- return HttpResponse(
- '<!DOCTYPE html><html><head><title></title></head><body>'
- '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
- # escape() calls force_text.
- (escape(pk_value), escapejs(obj)))
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -913,30 +943,36 @@ class ModelAdmin(BaseModelAdmin):
Determines the HttpResponse for the change_view stage.
"""
opts = self.model._meta
-
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
if "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- if "_popup" in request.REQUEST:
- return HttpResponseRedirect(request.path + "?_popup=1")
- else:
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_saveasnew" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_change' %
- (opts.app_label, opts.model_name),
- args=(pk_value,),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_change' %
+ (opts.app_label, opts.model_name),
+ args=(pk_value,),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_add' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_add' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -952,6 +988,8 @@ class ModelAdmin(BaseModelAdmin):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -963,10 +1001,13 @@ class ModelAdmin(BaseModelAdmin):
when editing an existing object.
"""
opts = self.model._meta
+
if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -1122,6 +1163,7 @@ class ModelAdmin(BaseModelAdmin):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True)
@@ -1214,6 +1256,7 @@ class ModelAdmin(BaseModelAdmin):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
@@ -1357,11 +1400,13 @@ class ModelAdmin(BaseModelAdmin):
'cl': cl,
'media': media,
'has_add_permission': self.has_add_permission(request),
+ 'opts': cl.opts,
'app_label': app_label,
'action_form': action_form,
'actions_on_top': self.actions_on_top,
'actions_on_bottom': self.actions_on_bottom,
'actions_selection_counter': self.actions_selection_counter,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1406,12 +1451,16 @@ class ModelAdmin(BaseModelAdmin):
'obj': force_text(obj_display)},
messages.SUCCESS)
- if not self.has_change_permission(request, None):
- return HttpResponseRedirect(reverse('admin:index',
- current_app=self.admin_site.name))
- return HttpResponseRedirect(reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ if self.has_change_permission(request, None):
+ post_url = reverse('admin:%s_%s_changelist' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
+ else:
+ post_url = reverse('admin:index',
+ current_app=self.admin_site.name)
+ return HttpResponseRedirect(post_url)
object_name = force_text(opts.verbose_name)
@@ -1429,6 +1478,7 @@ class ModelAdmin(BaseModelAdmin):
"protected": protected,
"opts": opts,
"app_label": app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1463,6 +1513,7 @@ class ModelAdmin(BaseModelAdmin):
'object': obj,
'app_label': app_label,
'opts': opts,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return TemplateResponse(request, self.object_history_template or [
@@ -1574,13 +1625,13 @@ class InlineModelAdmin(BaseModelAdmin):
'class_name': p._meta.verbose_name,
'instance': p}
)
- msg_dict = {'class_name': self._meta.model._meta.verbose_name,
- 'instance': self.instance,
- 'related_objects': get_text_list(objs, _('and'))}
+ params = {'class_name': self._meta.model._meta.verbose_name,
+ 'instance': self.instance,
+ 'related_objects': get_text_list(objs, _('and'))}
msg = _("Deleting %(class_name)s %(instance)s would require "
"deleting the following protected related objects: "
- "%(related_objects)s") % msg_dict
- raise ValidationError(msg)
+ "%(related_objects)s")
+ raise ValidationError(msg, code='deleting_protected', params=params)
def is_valid(self):
result = super(DeleteProtectedModelForm, self).is_valid()
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 4accf80c46..d4935ed781 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_modify %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_modify %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
@@ -29,7 +28,10 @@
{% if change %}{% if not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
- <li><a href="{% url opts|admin_urlname:'history' original.pk|admin_urlquote %}" class="historylink">{% trans "History" %}</a></li>
+ <li>
+ {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
+ <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
+ </li>
{% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %}
</ul>
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 5d1a6b2714..15583d1a8c 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_list %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_list %}
{% block extrastyle %}
{{ block.super }}
@@ -54,7 +53,8 @@
<ul class="object-tools">
{% block object-tools-items %}
<li>
- <a href="{% url cl.opts|admin_urlname:'add' %}{% if is_popup %}?_popup=1{% endif %}" class="addlink">
+ {% url cl.opts|admin_urlname:'add' as add_url %}
+ <a href="{% add_preserved_filters add_url is_popup %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
@@ -64,7 +64,7 @@
{% endblock %}
{% if cl.formset.errors %}
<p class="errornote">
- {% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
+ {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{{ cl.formset.non_form_errors }}
{% endif %}
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index c1a711534d..3ed60807b7 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n %}
-{% load admin_urls %}
+{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
index 608052d6f5..36527dcbf5 100644
--- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n l10n %}
-{% load admin_urls %}
+{% load i18n l10n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html
index 870c4648a6..c750ff14b3 100644
--- a/django/contrib/admin/templates/admin/object_history.html
+++ b/django/contrib/admin/templates/admin/object_history.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n %}
-{% load admin_urls %}
+{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html
index 38a97a1c6a..52baed3ffa 100644
--- a/django/contrib/admin/templates/admin/submit_line.html
+++ b/django/contrib/admin/templates/admin/submit_line.html
@@ -1,7 +1,10 @@
{% load i18n admin_urls %}
<div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
-{% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
+{% if show_delete_link %}
+ {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
+ <p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
+{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 965352e0f5..e81b13cda4 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.util import (lookup_field, display_for_field,
display_for_value, label_for_field)
from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
@@ -217,6 +218,7 @@ def items_for_result(cl, result, form):
table_tag = {True:'th', False:'td'}[first]
first = False
url = cl.url_for_result(result)
+ url = add_preserved_filters({'preserved_filters': cl.preserved_filters, 'opts': cl.opts}, url)
# Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings.
if cl.to_field:
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index cecc6ed6c4..98ac1a657e 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -37,7 +37,8 @@ def submit_row(context):
not is_popup and (not save_as or context['add']),
'show_save_and_continue': not is_popup and context['has_change_permission'],
'is_popup': is_popup,
- 'show_save': True
+ 'show_save': True,
+ 'preserved_filters': context.get('preserved_filters'),
}
if context.get('original') is not None:
ctx['original'] = context['original']
diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py
index bca95d92ae..bb1b16883d 100644
--- a/django/contrib/admin/templatetags/admin_urls.py
+++ b/django/contrib/admin/templatetags/admin_urls.py
@@ -1,8 +1,17 @@
+from django.utils.http import urlencode
+
+try:
+ from urllib.parse import parse_qsl, urlparse, urlunparse
+except ImportError:
+ from urlparse import parse_qsl, urlparse, urlunparse
+
from django import template
from django.contrib.admin.util import quote
+from django.core.urlresolvers import resolve, Resolver404
register = template.Library()
+
@register.filter
def admin_urlname(value, arg):
return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg)
@@ -11,3 +20,36 @@ def admin_urlname(value, arg):
@register.filter
def admin_urlquote(value):
return quote(value)
+
+
+@register.simple_tag(takes_context=True)
+def add_preserved_filters(context, url, popup=False):
+ opts = context.get('opts')
+ preserved_filters = context.get('preserved_filters')
+
+ parsed_url = list(urlparse(url))
+ parsed_qs = dict(parse_qsl(parsed_url[4]))
+ merged_qs = dict()
+
+ if opts and preserved_filters:
+ preserved_filters = dict(parse_qsl(preserved_filters))
+
+ try:
+ match = resolve(url)
+ except Resolver404:
+ pass
+ else:
+ current_url = '%s:%s' % (match.namespace, match.url_name)
+ changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
+ if changelist_url == current_url and '_changelist_filters' in preserved_filters:
+ preserved_filters = dict(parse_qsl(preserved_filters['_changelist_filters']))
+
+ merged_qs.update(preserved_filters)
+
+ if popup:
+ merged_qs['_popup'] = 1
+
+ merged_qs.update(parsed_qs)
+
+ parsed_url[4] = urlencode(merged_qs)
+ return urlunparse(parsed_url)
diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py
index badf45b580..25ea230b28 100644
--- a/django/contrib/admin/tests.py
+++ b/django/contrib/admin/tests.py
@@ -5,7 +5,16 @@ from django.utils.module_loading import import_by_path
from django.utils.unittest import SkipTest
from django.utils.translation import ugettext as _
+
class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
+
+ available_apps = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ ]
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
@classmethod
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 8ea7e10fc0..f676706a89 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -59,6 +59,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
self.list_per_page = list_per_page
self.list_max_show_all = list_max_show_all
self.model_admin = model_admin
+ self.preserved_filters = model_admin.get_preserved_filters(request)
# Get search parameters from the query string.
try:
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index edf2727b07..a9ecba45c2 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -14,7 +14,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.models import User
-from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site
@@ -29,7 +29,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
encoded = value
final_attrs = self.build_attrs(attrs)
- if not encoded or encoded == UNUSABLE_PASSWORD:
+ if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
else:
try:
@@ -97,14 +97,19 @@ class UserCreationForm(forms.ModelForm):
User._default_manager.get(username=username)
except User.DoesNotExist:
return username
- raise forms.ValidationError(self.error_messages['duplicate_username'])
+ raise forms.ValidationError(
+ self.error_messages['duplicate_username'],
+ code='duplicate_username',
+ )
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -183,11 +188,15 @@ class AuthenticationForm(forms.Form):
password=password)
if self.user_cache is None:
raise forms.ValidationError(
- self.error_messages['invalid_login'] % {
- 'username': self.username_field.verbose_name
- })
+ self.error_messages['invalid_login'],
+ code='invalid_login',
+ params={'username': self.username_field.verbose_name},
+ )
elif not self.user_cache.is_active:
- raise forms.ValidationError(self.error_messages['inactive'])
+ raise forms.ValidationError(
+ self.error_messages['inactive'],
+ code='inactive',
+ )
return self.cleaned_data
def check_for_test_cookie(self):
@@ -222,7 +231,7 @@ class PasswordResetForm(forms.Form):
for user in users:
# Make sure that no email is sent to a user that actually has
# a password marked as unusable
- if user.password == UNUSABLE_PASSWORD:
+ if not user.has_usable_password():
continue
if not domain_override:
current_site = get_current_site(request)
@@ -269,7 +278,9 @@ class SetPasswordForm(forms.Form):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -298,7 +309,9 @@ class PasswordChangeForm(SetPasswordForm):
old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password):
raise forms.ValidationError(
- self.error_messages['password_incorrect'])
+ self.error_messages['password_incorrect'],
+ code='password_incorrect',
+ )
return old_password
PasswordChangeForm.base_fields = SortedDict([
@@ -329,7 +342,9 @@ class AdminPasswordChangeForm(forms.Form):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 6abdb5f476..87e4218a8f 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -17,7 +17,8 @@ from django.utils.module_loading import import_by_path
from django.utils.translation import ugettext_noop as _
-UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
+UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
+UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
HASHERS = None # lazily loaded from PASSWORD_HASHERS
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
@@ -30,7 +31,7 @@ def reset_hashers(**kwargs):
def is_password_usable(encoded):
- if encoded is None or encoded == UNUSABLE_PASSWORD:
+ if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
return False
try:
hasher = identify_hasher(encoded)
@@ -47,7 +48,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
If setter is specified, it'll be called when you need to
regenerate the password.
"""
- if not password or not is_password_usable(encoded):
+ if not is_password_usable(encoded):
return False
preferred = get_hasher(preferred)
@@ -64,13 +65,15 @@ def make_password(password, salt=None, hasher='default'):
"""
Turn a plain-text password into a hash for database storage
- Same as encode() but generates a new random salt. If
- password is None or blank then UNUSABLE_PASSWORD will be
- returned which disallows logins.
+ Same as encode() but generates a new random salt.
+ If password is None then a concatenation of
+ UNUSABLE_PASSWORD_PREFIX and a random string will be returned
+ which disallows logins. Additional random string reduces chances
+ of gaining access to staff or superuser accounts.
+ See ticket #20079 for more info.
"""
- if not password:
- return UNUSABLE_PASSWORD
-
+ if password is None:
+ return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher)
if not salt:
@@ -171,12 +174,12 @@ class BasePasswordHasher(object):
name = mod_path = self.library
try:
module = importlib.import_module(mod_path)
- except ImportError:
- raise ValueError("Couldn't load %s password algorithm "
- "library" % name)
+ except ImportError as e:
+ raise ValueError("Couldn't load %r algorithm library: %s" %
+ (self.__class__.__name__, e))
return module
- raise ValueError("Hasher '%s' doesn't specify a library attribute" %
- self.__class__)
+ raise ValueError("Hasher %r doesn't specify a library attribute" %
+ self.__class__.__name__)
def salt(self):
"""
@@ -222,7 +225,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
digest = hashlib.sha256
def encode(self, password, salt, iterations=None):
- assert password
+ assert password is not None
assert salt and '$' not in salt
if not iterations:
iterations = self.iterations
@@ -350,7 +353,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
algorithm = "sha1"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
@@ -378,7 +381,7 @@ class MD5PasswordHasher(BasePasswordHasher):
algorithm = "md5"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
index fdf822ff74..ecf6c1b635 100644
--- a/django/contrib/auth/management/__init__.py
+++ b/django/contrib/auth/management/__init__.py
@@ -11,7 +11,7 @@ from django.contrib.auth import models as auth_app, get_user_model
from django.core import exceptions
from django.core.management.base import CommandError
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_models, signals
+from django.db.models import get_model, get_models, signals, UnavailableApp
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
from django.utils import six
from django.utils.six.moves import input
@@ -60,6 +60,11 @@ def _check_permission_clashing(custom, builtin, ctype):
pool.add(codename)
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
+ try:
+ get_model('auth', 'Permission')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, auth_app.Permission):
return
@@ -101,9 +106,13 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
def create_superuser(app, created_models, verbosity, db, **kwargs):
- from django.core.management import call_command
+ try:
+ get_model('auth', 'Permission')
+ UserModel = get_user_model()
+ except UnavailableApp:
+ return
- UserModel = get_user_model()
+ from django.core.management import call_command
if UserModel in created_models and kwargs.get('interactive', True):
msg = ("\nYou just installed Django's auth system, which means you "
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index 798cc805a0..f6380b9f24 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -16,7 +16,7 @@ from django.utils import timezone
from django.contrib import auth
# UNUSABLE_PASSWORD is still imported here for backwards compatibility
from django.contrib.auth.hashers import (
- check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
+ check_password, make_password, is_password_usable)
from django.contrib.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import python_2_unicode_compatible
diff --git a/django/contrib/auth/tests/test_custom_user.py b/django/contrib/auth/tests/test_custom_user.py
index a3a159880a..0f6ebbda55 100644
--- a/django/contrib/auth/tests/test_custom_user.py
+++ b/django/contrib/auth/tests/test_custom_user.py
@@ -156,6 +156,18 @@ class CustomUserNonUniqueUsername(AbstractBaseUser):
app_label = 'auth'
+class CustomUserNonListRequiredFields(AbstractBaseUser):
+ "A user with a non-list REQUIRED_FIELDS"
+ username = models.CharField(max_length=30, unique=True)
+ date_of_birth = models.DateField()
+
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = 'date_of_birth'
+
+ class Meta:
+ app_label = 'auth'
+
+
class CustomUserBadRequiredFields(AbstractBaseUser):
"A user with a non-unique username"
username = models.CharField(max_length=30, unique=True)
diff --git a/django/contrib/auth/tests/test_handlers.py b/django/contrib/auth/tests/test_handlers.py
index e0d2fa2200..b3737172a3 100644
--- a/django/contrib/auth/tests/test_handlers.py
+++ b/django/contrib/auth/tests/test_handlers.py
@@ -8,10 +8,18 @@ from django.test import TransactionTestCase
from django.test.utils import override_settings
+# This must be a TransactionTestCase because the WSGI auth handler performs
+# its own transaction management.
class ModWsgiHandlerTestCase(TransactionTestCase):
"""
Tests for the mod_wsgi authentication handler
"""
+
+ available_apps = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ ]
+
@skipIfCustomUser
def test_check_password(self):
"""
diff --git a/django/contrib/auth/tests/test_hashers.py b/django/contrib/auth/tests/test_hashers.py
index d49fdc412e..9b7811a335 100644
--- a/django/contrib/auth/tests/test_hashers.py
+++ b/django/contrib/auth/tests/test_hashers.py
@@ -2,9 +2,10 @@
from __future__ import unicode_literals
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
-from django.contrib.auth.hashers import (is_password_usable,
- check_password, make_password, PBKDF2PasswordHasher, load_hashers,
- PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
+from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
+ check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher,
+ get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
+from django.utils import six
from django.utils import unittest
from django.utils.unittest import skipUnless
@@ -31,6 +32,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
+ # Blank passwords
+ blank_encoded = make_password('')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_pkbdf2(self):
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@@ -40,6 +47,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_sha1(self):
encoded = make_password('lètmein', 'seasalt', 'sha1')
@@ -49,6 +62,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_md5(self):
encoded = make_password('lètmein', 'seasalt', 'md5')
@@ -58,6 +77,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'md5')
+ self.assertTrue(blank_encoded.startswith('md5$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_md5(self):
encoded = make_password('lètmein', '', 'unsalted_md5')
@@ -71,6 +96,11 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(alt_encoded))
self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_md5')
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1')
@@ -82,6 +112,12 @@ class TestUtilsHashPass(unittest.TestCase):
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
@@ -91,6 +127,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmei', encoded))
self.assertFalse(check_password('lètmeiz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
+ # Blank passwords
+ blank_encoded = make_password('', 'ab', 'crypt')
+ self.assertTrue(blank_encoded.startswith('crypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt_sha256(self):
@@ -107,6 +149,12 @@ class TestUtilsHashPass(unittest.TestCase):
encoded = make_password(password, hasher='bcrypt_sha256')
self.assertTrue(check_password(password, encoded))
self.assertFalse(check_password(password[:72], encoded))
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt_sha256')
+ self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt(self):
@@ -116,21 +164,31 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt')
+ self.assertTrue(blank_encoded.startswith('bcrypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unusable(self):
encoded = make_password(None)
+ self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
self.assertFalse(is_password_usable(encoded))
self.assertFalse(check_password(None, encoded))
- self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded))
+ self.assertFalse(check_password(encoded, encoded))
+ self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
self.assertFalse(check_password('', encoded))
self.assertFalse(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertRaises(ValueError, identify_hasher, encoded)
+ # Assert that the unusable passwords actually contain a random part.
+ # This might fail one day due to a hash collision.
+ self.assertNotEqual(encoded, make_password(None), "Random password collision?")
def test_bad_algorithm(self):
- def doit():
+ with self.assertRaises(ValueError):
make_password('lètmein', hasher='lolcat')
- self.assertRaises(ValueError, doit)
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
def test_bad_encoded(self):
@@ -178,3 +236,17 @@ class TestUtilsHashPass(unittest.TestCase):
state['upgraded'] = True
self.assertFalse(check_password('WRONG', encoded, setter))
self.assertFalse(state['upgraded'])
+
+ def test_load_library_no_algorithm(self):
+ with self.assertRaises(ValueError) as e:
+ BasePasswordHasher()._load_library()
+ self.assertEqual("Hasher 'BasePasswordHasher' doesn't specify a "
+ "library attribute", str(e.exception))
+
+ def test_load_library_importerror(self):
+ PlainHasher = type(str('PlainHasher'), (BasePasswordHasher,),
+ {'algorithm': 'plain', 'library': 'plain'})
+ # Python 3.3 adds quotes around module name
+ with six.assertRaisesRegex(self, ValueError,
+ "Couldn't load 'PlainHasher' algorithm library: No module named '?plain'?"):
+ PlainHasher()._load_library()
diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py
index fee0a29e7b..6c3718465d 100644
--- a/django/contrib/auth/tests/test_management.py
+++ b/django/contrib/auth/tests/test_management.py
@@ -174,6 +174,13 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
class CustomUserModelValidationTestCase(TestCase):
+ @override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
+ def test_required_fields_is_list(self):
+ "REQUIRED_FIELDS should be a list."
+ new_io = StringIO()
+ get_validation_errors(new_io, get_app('auth'))
+ self.assertIn("The REQUIRED_FIELDS must be a list or tuple.", new_io.getvalue())
+
@override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields')
def test_username_not_in_required_fields(self):
"USERNAME_FIELD should not appear in REQUIRED_FIELDS."
diff --git a/django/contrib/auth/tests/test_models.py b/django/contrib/auth/tests/test_models.py
index 8ac0599e6b..cf412c96e6 100644
--- a/django/contrib/auth/tests/test_models.py
+++ b/django/contrib/auth/tests/test_models.py
@@ -87,7 +87,7 @@ class UserManagerTestCase(TestCase):
user = User.objects.create_user('user', email_lowercase)
self.assertEqual(user.email, email_lowercase)
self.assertEqual(user.username, 'user')
- self.assertEqual(user.password, '!')
+ self.assertFalse(user.has_usable_password())
def test_create_user_email_domain_normalize_rfc3696(self):
# According to http://tools.ietf.org/html/rfc3696#section-3
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index ddd7654ed7..64d1c418ef 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_apps, get_models, signals
+from django.db.models import get_apps, get_model, get_models, signals, UnavailableApp
from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
@@ -11,6 +11,11 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
Creates content types for models in the given app, removing any model
entries that no longer have a matching model class.
"""
+ try:
+ get_model('contenttypes', 'ContentType')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, ContentType):
return
diff --git a/django/contrib/flatpages/forms.py b/django/contrib/flatpages/forms.py
index 80938116ad..a93a494096 100644
--- a/django/contrib/flatpages/forms.py
+++ b/django/contrib/flatpages/forms.py
@@ -17,11 +17,17 @@ class FlatpageForm(forms.ModelForm):
def clean_url(self):
url = self.cleaned_data['url']
if not url.startswith('/'):
- raise forms.ValidationError(ugettext("URL is missing a leading slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a leading slash."),
+ code='missing_leading_slash',
+ )
if (settings.APPEND_SLASH and
'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and
not url.endswith('/')):
- raise forms.ValidationError(ugettext("URL is missing a trailing slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a trailing slash."),
+ code='missing_trailing_slash',
+ )
return url
def clean(self):
@@ -36,7 +42,9 @@ class FlatpageForm(forms.ModelForm):
for site in sites:
if same_url.filter(sites=site).exists():
raise forms.ValidationError(
- _('Flatpage with url %(url)s already exists for site %(site)s') %
- {'url': url, 'site': site})
+ _('Flatpage with url %(url)s already exists for site %(site)s'),
+ code='duplicate_url',
+ params={'url': url, 'site': site},
+ )
return super(FlatpageForm, self).clean()
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
index 17cfa6baa7..c478f20854 100644
--- a/django/contrib/formtools/wizard/views.py
+++ b/django/contrib/formtools/wizard/views.py
@@ -7,6 +7,7 @@ from django.forms import formsets, ValidationError
from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod
+from django.utils.translation import ugettext as _
from django.utils import six
from django.contrib.formtools.wizard.storage import get_storage
@@ -271,7 +272,9 @@ class WizardView(TemplateView):
management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid():
raise ValidationError(
- 'ManagementForm data is missing or has been tampered.')
+ _('ManagementForm data is missing or has been tampered.'),
+ code='missing_management_form',
+ )
form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and
diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py
index 6e2cbd59f5..59e725926c 100644
--- a/django/contrib/gis/forms/fields.py
+++ b/django/contrib/gis/forms/fields.py
@@ -50,7 +50,7 @@ class GeometryField(forms.Field):
try:
return GEOSGeometry(value)
except (GEOSException, ValueError, TypeError):
- raise forms.ValidationError(self.error_messages['invalid_geom'])
+ raise forms.ValidationError(self.error_messages['invalid_geom'], code='invalid_geom')
def clean(self, value):
"""
@@ -65,7 +65,7 @@ class GeometryField(forms.Field):
# Ensuring that the geometry is of the correct type (indicated
# using the OGC string label).
if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY':
- raise forms.ValidationError(self.error_messages['invalid_geom_type'])
+ raise forms.ValidationError(self.error_messages['invalid_geom_type'], code='invalid_geom_type')
# Transforming the geometry if the SRID was set.
if self.srid:
@@ -76,7 +76,7 @@ class GeometryField(forms.Field):
try:
geom.transform(self.srid)
except:
- raise forms.ValidationError(self.error_messages['transform_error'])
+ raise forms.ValidationError(self.error_messages['transform_error'], code='transform_error')
return geom
diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py
index 879497deb3..bbd85ed3f6 100644
--- a/django/contrib/sites/models.py
+++ b/django/contrib/sites/models.py
@@ -22,7 +22,9 @@ def _simple_domain_name_validator(value):
checks = ((s in value) for s in string.whitespace)
if any(checks):
raise ValidationError(
- _("The domain name cannot contain any spaces or tabs."))
+ _("The domain name cannot contain any spaces or tabs."),
+ code='invalid',
+ )
class SiteManager(models.Manager):
diff --git a/django/core/compat_checks/__init__.py b/django/core/compat_checks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/core/compat_checks/__init__.py
diff --git a/django/core/compat_checks/base.py b/django/core/compat_checks/base.py
new file mode 100644
index 0000000000..e54b50f287
--- /dev/null
+++ b/django/core/compat_checks/base.py
@@ -0,0 +1,39 @@
+from __future__ import unicode_literals
+import warnings
+
+from django.core.compat_checks import django_1_6_0
+
+
+COMPAT_CHECKS = [
+ # Add new modules at the top, so we keep things in descending order.
+ # After two-three minor releases, old versions should get dropped.
+ django_1_6_0,
+]
+
+
+def check_compatibility():
+ """
+ Runs through compatibility checks to warn the user with an existing install
+ about changes in an up-to-date Django.
+
+ Modules should be located in ``django.core.compat_checks`` (typically one
+ per release of Django) & must have a ``run_checks`` function that runs
+ all the checks.
+
+ Returns a list of informational messages about incompatibilities.
+ """
+ messages = []
+
+ for check_module in COMPAT_CHECKS:
+ check = getattr(check_module, 'run_checks', None)
+
+ if check is None:
+ warnings.warn(
+ "The '%s' module lacks a " % check_module.__name__ +
+ "'run_checks' method, which is needed to verify compatibility."
+ )
+ continue
+
+ messages.extend(check())
+
+ return messages
diff --git a/django/core/compat_checks/django_1_6_0.py b/django/core/compat_checks/django_1_6_0.py
new file mode 100644
index 0000000000..bb0dabedac
--- /dev/null
+++ b/django/core/compat_checks/django_1_6_0.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+
+def check_test_runner():
+ """
+ Checks if the user has *not* overridden the ``TEST_RUNNER`` setting &
+ warns them about the default behavior changes.
+
+ If the user has overridden that setting, we presume they know what they're
+ doing & avoid generating a message.
+ """
+ from django.conf import settings
+ new_default = 'django.test.runner.DiscoverRunner'
+ test_runner_setting = getattr(settings, 'TEST_RUNNER', new_default)
+
+ if test_runner_setting == new_default:
+ message = [
+ "You have not explicitly set 'TEST_RUNNER'. In Django 1.6,",
+ "there is a new test runner ('%s')" % new_default,
+ "by default. You should ensure your tests are still all",
+ "running & behaving as expected. See",
+ "https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module",
+ "for more information.",
+ ]
+ return ' '.join(message)
+
+
+def run_checks():
+ """
+ Required by the ``checksetup`` management command, this returns a list of
+ messages from all the relevant check functions for this version of Django.
+ """
+ checks = [
+ check_test_runner()
+ ]
+ # Filter out the ``None`` or empty strings.
+ return [output for output in checks if output]
diff --git a/django/core/exceptions.py b/django/core/exceptions.py
index 2c79736e33..829d6e774e 100644
--- a/django/core/exceptions.py
+++ b/django/core/exceptions.py
@@ -3,6 +3,9 @@ Global Django exception and warning classes.
"""
import logging
from functools import reduce
+import operator
+
+from django.utils.encoding import force_text
class DjangoRuntimeWarning(RuntimeWarning):
@@ -74,46 +77,65 @@ NON_FIELD_ERRORS = '__all__'
class ValidationError(Exception):
"""An error while validating data."""
def __init__(self, message, code=None, params=None):
- import operator
- from django.utils.encoding import force_text
"""
ValidationError can be passed any object that can be printed (usually
a string), a list of objects or a dictionary.
"""
if isinstance(message, dict):
- self.message_dict = message
- # Reduce each list of messages into a single list.
- message = reduce(operator.add, message.values())
-
- if isinstance(message, list):
- self.messages = [force_text(msg) for msg in message]
+ self.error_dict = message
+ elif isinstance(message, list):
+ self.error_list = message
else:
self.code = code
self.params = params
+ self.message = message
+ self.error_list = [self]
+
+ @property
+ def message_dict(self):
+ message_dict = {}
+ for field, messages in self.error_dict.items():
+ message_dict[field] = []
+ for message in messages:
+ if isinstance(message, ValidationError):
+ message_dict[field].extend(message.messages)
+ else:
+ message_dict[field].append(force_text(message))
+ return message_dict
+
+ @property
+ def messages(self):
+ if hasattr(self, 'error_dict'):
+ message_list = reduce(operator.add, self.error_dict.values())
+ else:
+ message_list = self.error_list
+
+ messages = []
+ for message in message_list:
+ if isinstance(message, ValidationError):
+ params = message.params
+ message = message.message
+ if params:
+ message %= params
message = force_text(message)
- self.messages = [message]
+ messages.append(message)
+ return messages
def __str__(self):
- # This is needed because, without a __str__(), printing an exception
- # instance would result in this:
- # AttributeError: ValidationError instance has no attribute 'args'
- # See http://www.python.org/doc/current/tut/node10.html#handling
- if hasattr(self, 'message_dict'):
+ if hasattr(self, 'error_dict'):
return repr(self.message_dict)
return repr(self.messages)
def __repr__(self):
- if hasattr(self, 'message_dict'):
- return 'ValidationError(%s)' % repr(self.message_dict)
- return 'ValidationError(%s)' % repr(self.messages)
+ return 'ValidationError(%s)' % self
def update_error_dict(self, error_dict):
- if hasattr(self, 'message_dict'):
+ if hasattr(self, 'error_dict'):
if error_dict:
- for k, v in self.message_dict.items():
+ for k, v in self.error_dict.items():
error_dict.setdefault(k, []).extend(v)
else:
- error_dict = self.message_dict
+ error_dict = self.error_dict
else:
- error_dict[NON_FIELD_ERRORS] = self.messages
+ error_dict[NON_FIELD_ERRORS] = self.error_list
return error_dict
diff --git a/django/core/management/commands/checksetup.py b/django/core/management/commands/checksetup.py
new file mode 100644
index 0000000000..d37e826757
--- /dev/null
+++ b/django/core/management/commands/checksetup.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+import warnings
+
+from django.core.compat_checks.base import check_compatibility
+from django.core.management.base import NoArgsCommand
+
+
+class Command(NoArgsCommand):
+ help = "Checks your configuration's compatibility with this version " + \
+ "of Django."
+
+ def handle_noargs(self, **options):
+ for message in check_compatibility():
+ warnings.warn(message)
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index c56fc1e1b0..95dd634d08 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -32,8 +32,10 @@ class Command(NoArgsCommand):
connection = connections[db]
verbosity = int(options.get('verbosity'))
interactive = options.get('interactive')
- # 'reset_sequences' is a stealth option
+ # The following are stealth options used by Django's internals.
reset_sequences = options.get('reset_sequences', True)
+ allow_cascade = options.get('allow_cascade', False)
+ inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
self.style = no_style()
@@ -45,7 +47,9 @@ class Command(NoArgsCommand):
except ImportError:
pass
- sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)
+ sql_list = sql_flush(self.style, connection, only_django=True,
+ reset_sequences=reset_sequences,
+ allow_cascade=allow_cascade)
if interactive:
confirm = input("""You have requested a flush of the database.
@@ -72,16 +76,9 @@ Are you sure you want to do this?
"Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.\n"
"The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
- # Emit the post sync signal. This allows individual
- # applications to respond as if the database had been
- # sync'd from scratch.
- all_models = []
- for app in models.get_apps():
- all_models.extend([
- m for m in models.get_models(app, include_auto_created=True)
- if router.allow_syncdb(db, m)
- ])
- emit_post_sync_signal(set(all_models), verbosity, interactive, db)
+
+ if not inhibit_post_syncdb:
+ self.emit_post_syncdb(verbosity, interactive, db)
# Reinstall the initial_data fixture.
if options.get('load_initial_data'):
@@ -90,3 +87,15 @@ Are you sure you want to do this?
else:
self.stdout.write("Flush cancelled.\n")
+
+ @staticmethod
+ def emit_post_syncdb(verbosity, interactive, database):
+ # Emit the post sync signal. This allows individual applications to
+ # respond as if the database had been sync'd from scratch.
+ all_models = []
+ for app in models.get_apps():
+ all_models.extend([
+ m for m in models.get_models(app, include_auto_created=True)
+ if router.allow_syncdb(database, m)
+ ])
+ emit_post_sync_signal(set(all_models), verbosity, interactive, database)
diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py
index c4a0b78cd4..f5a701cd23 100644
--- a/django/core/management/commands/runserver.py
+++ b/django/core/management/commands/runserver.py
@@ -40,6 +40,11 @@ class Command(BaseCommand):
return get_internal_wsgi_application()
def handle(self, addrport='', *args, **options):
+ from django.conf import settings
+
+ if not settings.DEBUG and not settings.ALLOWED_HOSTS:
+ raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')
+
self.use_ipv6 = options.get('use_ipv6')
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 42ccafa2c5..b58d89f60a 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -102,7 +102,7 @@ def sql_delete(app, style, connection):
return output[::-1] # Reverse it, to deal with table dependencies.
-def sql_flush(style, connection, only_django=False, reset_sequences=True):
+def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_cascade=False):
"""
Returns a list of the SQL statements used to flush the database.
@@ -114,7 +114,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True):
else:
tables = connection.introspection.table_names()
seqs = connection.introspection.sequence_list() if reset_sequences else ()
- statements = connection.ops.sql_flush(style, tables, seqs)
+ statements = connection.ops.sql_flush(style, tables, seqs, allow_cascade)
return statements
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index a6d6a76985..a64c6e815c 100644
--- a/django/core/management/validation.py
+++ b/django/core/management/validation.py
@@ -51,6 +51,10 @@ def get_validation_errors(outfile, app=None):
# If this is the current User model, check known validation problems with User models
if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name):
+ # Check that REQUIRED_FIELDS is a list
+ if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
+ e.add(opts, 'The REQUIRED_FIELDS must be a list or tuple.')
+
# Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
index d58f2a9fa3..b7017e47b9 100644
--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -422,8 +422,11 @@ class RegexURLResolver(LocaleRegexProvider):
lookup_view_s = "%s.%s" % (m, n)
else:
lookup_view_s = lookup_view
+
+ patterns = [pattern for (possibility, pattern, defaults) in possibilities]
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
- "arguments '%s' not found." % (lookup_view_s, args, kwargs))
+ "arguments '%s' not found. %d pattern(s) tried: %s" %
+ (lookup_view_s, args, kwargs, len(patterns), patterns))
class LocaleRegexURLResolver(RegexURLResolver):
"""
diff --git a/django/core/validators.py b/django/core/validators.py
index d0b713be32..200d28fe02 100644
--- a/django/core/validators.py
+++ b/django/core/validators.py
@@ -76,7 +76,7 @@ def validate_integer(value):
try:
int(value)
except (ValueError, TypeError):
- raise ValidationError('')
+ raise ValidationError(_('Enter a valid integer.'), code='invalid')
class EmailValidator(object):
@@ -188,11 +188,7 @@ class BaseValidator(object):
cleaned = self.clean(value)
params = {'limit_value': self.limit_value, 'show_value': cleaned}
if self.compare(cleaned, self.limit_value):
- raise ValidationError(
- self.message % params,
- code=self.code,
- params=params,
- )
+ raise ValidationError(self.message, code=self.code, params=params)
class MaxValueValidator(BaseValidator):
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 1b8e6ae447..2fcf6b9604 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -390,7 +390,7 @@ class BaseDatabaseWrapper(object):
def disable_constraint_checking(self):
"""
Backends can implement as needed to temporarily disable foreign key
- constraint checking. Should return True if the constraints were
+ constraint checking. Should return True if the constraints were
disabled and will need to be reenabled.
"""
return False
@@ -966,7 +966,7 @@ class BaseDatabaseOperations(object):
"""
return ''
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
"""
Returns a list of SQL statements required to remove all data from
the given database tables (without actually removing the tables
@@ -977,6 +977,10 @@ class BaseDatabaseOperations(object):
The `style` argument is a Style object as returned by either
color_style() or no_style() in django.core.management.color.
+
+ The `allow_cascade` argument determines whether truncation may cascade
+ to tables with foreign keys pointing the tables being truncated.
+ PostgreSQL requires a cascade even if these tables are empty.
"""
raise NotImplementedError()
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index fd9655077e..08b8106ddd 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -302,14 +302,17 @@ class DatabaseOperations(BaseDatabaseOperations):
def random_function_sql(self):
return 'RAND()'
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# NB: The generated SQL below is specific to MySQL
# 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
# to clear all tables of all data
if tables:
sql = ['SET FOREIGN_KEY_CHECKS = 0;']
for table in tables:
- sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table))))
+ sql.append('%s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(self.quote_name(table)),
+ ))
sql.append('SET FOREIGN_KEY_CHECKS = 1;')
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 798c735d7b..3105ad17e3 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -340,17 +340,17 @@ WHEN (new.%(col_name)s IS NULL)
def savepoint_rollback_sql(self, sid):
return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(sid))
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# Return a list of 'TRUNCATE x;', 'TRUNCATE y;',
# 'TRUNCATE z;'... style SQL statements
if tables:
# Oracle does support TRUNCATE, but it seems to get us into
# FK referential trouble, whereas DELETE FROM table works.
- sql = ['%s %s %s;' % \
- (style.SQL_KEYWORD('DELETE'),
- style.SQL_KEYWORD('FROM'),
- style.SQL_FIELD(self.quote_name(table)))
- for table in tables]
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(self.quote_name(table))
+ ) for table in tables]
# Since we've just deleted all the rows, running our sequence
# ALTER code will reset the sequence to 0.
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py
index f06eec5a1d..f96757da8b 100644
--- a/django/db/backends/postgresql_psycopg2/operations.py
+++ b/django/db/backends/postgresql_psycopg2/operations.py
@@ -101,15 +101,24 @@ class DatabaseOperations(BaseDatabaseOperations):
def set_time_zone_sql(self):
return "SET TIME ZONE %s"
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
if tables:
# Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows
# us to truncate tables referenced by a foreign key in any other
# table.
- sql = ['%s %s;' % \
- (style.SQL_KEYWORD('TRUNCATE'),
- style.SQL_FIELD(', '.join([self.quote_name(table) for table in tables]))
- )]
+ tables_sql = ', '.join(
+ style.SQL_FIELD(self.quote_name(table)) for table in tables)
+ if allow_cascade:
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ tables_sql,
+ style.SQL_KEYWORD('CASCADE'),
+ )]
+ else:
+ sql = ['%s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ tables_sql,
+ )]
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
else:
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index f1ebd3f408..a53d3ee53e 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -212,15 +212,15 @@ class DatabaseOperations(BaseDatabaseOperations):
def no_limit_value(self):
return -1
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# NB: The generated SQL below is specific to SQLite
# Note: The DELETE FROM... SQL generated below works for SQLite databases
# because constraints don't exist
- sql = ['%s %s %s;' % \
- (style.SQL_KEYWORD('DELETE'),
- style.SQL_KEYWORD('FROM'),
- style.SQL_FIELD(self.quote_name(table))
- ) for table in tables]
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(self.quote_name(table))
+ ) for table in tables]
# Note: No requirement for reset of auto-incremented indices (cf. other
# sql_flush() implementations). Just return SQL at this point
return sql
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 3eac2167d4..b5dd1a58bc 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
+from django.db.models.loading import get_apps, 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/base.py b/django/db/models/base.py
index 5f1c21c255..17a4a14b8d 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -450,16 +450,18 @@ class Model(six.with_metaclass(ModelBase)):
need to do things manually, as they're dynamically created classes and
only module-level classes can be pickled by the default path.
"""
- if not self._deferred:
- return super(Model, self).__reduce__()
data = self.__dict__
+ if not self._deferred:
+ class_id = self._meta.app_label, self._meta.object_name
+ return model_unpickle, (class_id, [], simple_class_factory), data
defers = []
for field in self._meta.fields:
if isinstance(self.__class__.__dict__.get(field.attname),
- DeferredAttribute):
+ DeferredAttribute):
defers.append(field.attname)
model = self._meta.proxy_for_model
- return (model_unpickle, (model, defers), data)
+ class_id = model._meta.app_label, model._meta.object_name
+ return (model_unpickle, (class_id, defers, deferred_class_factory), data)
def _get_pk_val(self, meta=None):
if not meta:
@@ -907,7 +909,7 @@ class Model(six.with_metaclass(ModelBase)):
'field_label': six.text_type(field_labels)
}
- def full_clean(self, exclude=None):
+ def full_clean(self, exclude=None, validate_unique=True):
"""
Calls clean_fields, clean, and validate_unique, on the model,
and raises a ``ValidationError`` for any errors that occurred.
@@ -929,13 +931,14 @@ class Model(six.with_metaclass(ModelBase)):
errors = e.update_error_dict(errors)
# Run unique checks, but only for fields that passed validation.
- for name in errors.keys():
- if name != NON_FIELD_ERRORS and name not in exclude:
- exclude.append(name)
- try:
- self.validate_unique(exclude=exclude)
- except ValidationError as e:
- errors = e.update_error_dict(errors)
+ if validate_unique:
+ for name in errors.keys():
+ if name != NON_FIELD_ERRORS and name not in exclude:
+ exclude.append(name)
+ try:
+ self.validate_unique(exclude=exclude)
+ except ValidationError as e:
+ errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)
@@ -960,7 +963,7 @@ class Model(six.with_metaclass(ModelBase)):
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
- errors[f.name] = e.messages
+ errors[f.name] = e.error_list
if errors:
raise ValidationError(errors)
@@ -1007,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
class Empty(object):
pass
+def simple_class_factory(model, attrs):
+ """
+ Needed for dynamic classes.
+ """
+ return model
-def model_unpickle(model, attrs):
+def model_unpickle(model_id, attrs, factory):
"""
Used to unpickle Model subclasses with deferred fields.
"""
- cls = deferred_class_factory(model, attrs)
+ if isinstance(model_id, tuple):
+ model = get_model(*model_id)
+ else:
+ # Backwards compat - the model was cached directly in earlier versions.
+ model = model_id
+ cls = factory(model, attrs)
return cls.__new__(cls)
model_unpickle.__safe_for_unpickle__ = True
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 691eeffb08..7f3242b213 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -77,7 +77,7 @@ class Field(object):
auto_creation_counter = -1
default_validators = [] # Default set of validators
default_error_messages = {
- 'invalid_choice': _('Value %r is not a valid choice.'),
+ 'invalid_choice': _('Value %(value)r is not a valid choice.'),
'null': _('This field cannot be null.'),
'blank': _('This field cannot be blank.'),
'unique': _('%(model_name)s with this %(field_label)s '
@@ -294,12 +294,9 @@ class Field(object):
v(value)
except exceptions.ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages:
- message = self.error_messages[e.code]
- if e.params:
- message = message % e.params
- errors.append(message)
- else:
- errors.extend(e.messages)
+ e.message = self.error_messages[e.code]
+ errors.extend(e.error_list)
+
if errors:
raise exceptions.ValidationError(errors)
@@ -322,14 +319,17 @@ class Field(object):
return
elif value == option_key:
return
- msg = self.error_messages['invalid_choice'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
if value is None and not self.null:
- raise exceptions.ValidationError(self.error_messages['null'])
+ raise exceptions.ValidationError(self.error_messages['null'], code='null')
if not self.blank and value in self.empty_values:
- raise exceptions.ValidationError(self.error_messages['blank'])
+ raise exceptions.ValidationError(self.error_messages['blank'], code='blank')
def clean(self, value, model_instance):
"""
@@ -678,7 +678,7 @@ class AutoField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be an integer."),
+ 'invalid': _("'%(value)s' value must be an integer."),
}
def __init__(self, *args, **kwargs):
@@ -702,8 +702,11 @@ class AutoField(Field):
try:
return int(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def validate(self, value, model_instance):
pass
@@ -732,7 +735,7 @@ class AutoField(Field):
class BooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be either True or False."),
+ 'invalid': _("'%(value)s' value must be either True or False."),
}
description = _("Boolean (Either True or False)")
@@ -757,8 +760,11 @@ class BooleanField(Field):
return True
if value in ('f', 'False', '0'):
return False
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
@@ -830,9 +836,9 @@ class CommaSeparatedIntegerField(CharField):
class DateField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid date format. It must be "
+ 'invalid': _("'%(value)s' value has an invalid date format. It must be "
"in YYYY-MM-DD format."),
- 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) "
+ 'invalid_date': _("'%(value)s' value has the correct format (YYYY-MM-DD) "
"but it is an invalid date."),
}
description = _("Date (without time)")
@@ -878,11 +884,17 @@ class DateField(Field):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_date'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_date'],
+ code='invalid_date',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -930,11 +942,11 @@ class DateField(Field):
class DateTimeField(DateField):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid format. It must be in "
+ 'invalid': _("'%(value)s' value has an invalid format. It must be in "
"YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."),
- 'invalid_date': _("'%s' value has the correct format "
+ 'invalid_date': _("'%(value)s' value has the correct format "
"(YYYY-MM-DD) but it is an invalid date."),
- 'invalid_datetime': _("'%s' value has the correct format "
+ 'invalid_datetime': _("'%(value)s' value has the correct format "
"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
"but it is an invalid date/time."),
}
@@ -969,19 +981,28 @@ class DateTimeField(DateField):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_datetime'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_datetime'],
+ code='invalid_datetime',
+ params={'value': value},
+ )
try:
parsed = parse_date(value)
if parsed is not None:
return datetime.datetime(parsed.year, parsed.month, parsed.day)
except ValueError:
- msg = self.error_messages['invalid_date'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_date'],
+ code='invalid_date',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -1027,7 +1048,7 @@ class DateTimeField(DateField):
class DecimalField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be a decimal number."),
+ 'invalid': _("'%(value)s' value must be a decimal number."),
}
description = _("Decimal number")
@@ -1053,8 +1074,11 @@ class DecimalField(Field):
try:
return decimal.Decimal(value)
except decimal.InvalidOperation:
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def _format(self, value):
if isinstance(value, six.string_types) or value is None:
@@ -1162,7 +1186,7 @@ class FilePathField(Field):
class FloatField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
+ 'invalid': _("'%(value)s' value must be a float."),
}
description = _("Floating point number")
@@ -1180,8 +1204,11 @@ class FloatField(Field):
try:
return float(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField}
@@ -1191,7 +1218,7 @@ class FloatField(Field):
class IntegerField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be an integer."),
+ 'invalid': _("'%(value)s' value must be an integer."),
}
description = _("Integer")
@@ -1215,8 +1242,11 @@ class IntegerField(Field):
try:
return int(value)
except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField}
@@ -1314,7 +1344,7 @@ class GenericIPAddressField(Field):
class NullBooleanField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value must be either None, True or False."),
+ 'invalid': _("'%(value)s' value must be either None, True or False."),
}
description = _("Boolean (Either True, False or None)")
@@ -1343,8 +1373,11 @@ class NullBooleanField(Field):
return True
if value in ('f', 'False', '0'):
return False
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the
@@ -1393,6 +1426,7 @@ class PositiveSmallIntegerField(IntegerField):
return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField):
+ default_validators = [validators.validate_slug]
description = _("Slug (up to %(max_length)s)")
def __init__(self, *args, **kwargs):
@@ -1445,9 +1479,9 @@ class TextField(Field):
class TimeField(Field):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _("'%s' value has an invalid format. It must be in "
+ 'invalid': _("'%(value)s' value has an invalid format. It must be in "
"HH:MM[:ss[.uuuuuu]] format."),
- 'invalid_time': _("'%s' value has the correct format "
+ 'invalid_time': _("'%(value)s' value has the correct format "
"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."),
}
description = _("Time")
@@ -1487,11 +1521,17 @@ class TimeField(Field):
if parsed is not None:
return parsed
except ValueError:
- msg = self.error_messages['invalid_time'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid_time'],
+ code='invalid_time',
+ params={'value': value},
+ )
- msg = self.error_messages['invalid'] % value
- raise exceptions.ValidationError(msg)
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'value': value},
+ )
def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add):
@@ -1520,12 +1560,12 @@ class TimeField(Field):
return super(TimeField, self).formfield(**defaults)
class URLField(CharField):
+ default_validators = [validators.URLValidator()]
description = _("URL")
def __init__(self, verbose_name=None, name=None, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
CharField.__init__(self, verbose_name, name, **kwargs)
- self.validators.append(validators.URLValidator())
def deconstruct(self):
name, path, args, kwargs = super(URLField, self).deconstruct()
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 754a97633b..64890fff47 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1194,8 +1194,11 @@ class ForeignKey(ForeignObject):
)
qs = qs.complex_filter(self.rel.limit_choices_to)
if not qs.exists():
- raise exceptions.ValidationError(self.error_messages['invalid'] % {
- 'model': self.rel.to._meta.verbose_name, 'pk': value})
+ raise exceptions.ValidationError(
+ self.error_messages['invalid'],
+ code='invalid',
+ params={'model': self.rel.to._meta.verbose_name, 'pk': value},
+ )
def get_attname(self):
return '%s_id' % self.name
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index 535df7ce80..c1e394327f 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -15,6 +15,8 @@ import os
__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
'load_app', 'app_cache_ready')
+class UnavailableApp(Exception):
+ pass
def _initialize():
"""
@@ -40,11 +42,12 @@ def _initialize():
# -- Everything below here is only used when populating the cache --
loads_installed = True,
- loaded = False,
- handled = {},
- postponed = [],
- nesting_level = 0,
- _get_models_cache = {},
+ loaded=False,
+ handled=set(),
+ postponed=[],
+ nesting_level=0,
+ _get_models_cache={},
+ available_apps=None,
)
@@ -111,7 +114,7 @@ class BaseAppCache(object):
Loads the app with the provided fully qualified name, and returns the
model module.
"""
- self.handled[app_name] = None
+ self.handled.add(app_name)
self.nesting_level += 1
app_module = import_module(app_name)
try:
@@ -157,12 +160,17 @@ class BaseAppCache(object):
"""
self._populate()
+ apps = self.app_store.items()
+ if self.available_apps is not None:
+ apps = [elt for elt in apps
+ if self._label_for(elt[0]) in self.available_apps]
+
# Ensure the returned list is always in the same order (with new apps
# added at the end). This avoids unstable ordering on the admin app
# list page, for example.
- apps = [(v, k) for k, v in self.app_store.items()]
- apps.sort()
- return [elt[1] for elt in apps]
+ apps = sorted(apps, key=lambda elt: elt[1])
+
+ return [elt[0] for elt in apps]
def get_app_paths(self):
"""
@@ -183,8 +191,12 @@ class BaseAppCache(object):
def get_app(self, app_label, emptyOK=False):
"""
- Returns the module containing the models for the given app_label. If
- the app has no models in it and 'emptyOK' is True, returns None.
+ Returns the module containing the models for the given app_label.
+
+ Returns None if the app has no models in it and emptyOK is True.
+
+ Raises UnavailableApp when set_available_apps() in in effect and
+ doesn't include app_label.
"""
self._populate()
imp.acquire_lock()
@@ -192,12 +204,11 @@ class BaseAppCache(object):
for app_name in settings.INSTALLED_APPS:
if app_label == app_name.split('.')[-1]:
mod = self.load_app(app_name, False)
- if mod is None:
- if emptyOK:
- return None
+ if mod is None and not emptyOK:
raise ImproperlyConfigured("App with label %s is missing a models.py module." % app_label)
- else:
- return mod
+ if self.available_apps is not None and app_label not in self.available_apps:
+ raise UnavailableApp("App with label %s isn't available." % app_label)
+ return mod
raise ImproperlyConfigured("App with label %s could not be found" % app_label)
finally:
imp.release_lock()
@@ -234,8 +245,13 @@ class BaseAppCache(object):
if not self.loads_installed:
only_installed = False
cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
+ model_list = None
try:
- return self._get_models_cache[cache_key]
+ model_list = self._get_models_cache[cache_key]
+ if self.available_apps is not None and only_installed:
+ model_list = [m for m in model_list
+ if m._meta.app_label in self.available_apps]
+ return model_list
except KeyError:
pass
self._populate()
@@ -260,6 +276,9 @@ class BaseAppCache(object):
(not model._meta.swapped or include_swapped))
)
self._get_models_cache[cache_key] = model_list
+ if self.available_apps is not None and only_installed:
+ model_list = [m for m in model_list
+ if m._meta.app_label in self.available_apps]
return model_list
def get_model(self, app_label, model_name,
@@ -269,6 +288,9 @@ class BaseAppCache(object):
model_name.
Returns None if no model is found.
+
+ Raises UnavailableApp when set_available_apps() in in effect and
+ doesn't include app_label.
"""
if not self.loads_installed:
only_installed = False
@@ -276,7 +298,13 @@ class BaseAppCache(object):
self._populate()
if only_installed and app_label not in self.app_labels:
return None
- return self.app_models.get(app_label, SortedDict()).get(model_name.lower())
+ if (self.available_apps is not None and only_installed
+ and app_label not in self.available_apps):
+ raise UnavailableApp("App with label %s isn't available." % app_label)
+ try:
+ return self.app_models[app_label][model_name.lower()]
+ except KeyError:
+ return None
def register_models(self, app_label, *models):
"""
@@ -301,6 +329,16 @@ class BaseAppCache(object):
model_dict[model_name] = model
self._get_models_cache.clear()
+ def set_available_apps(self, available):
+ if not set(available).issubset(set(settings.INSTALLED_APPS)):
+ extra = set(available) - set(settings.INSTALLED_APPS)
+ raise ValueError("Available apps isn't a subset of installed "
+ "apps, extra apps: " + ", ".join(extra))
+ self.available_apps = set(app.rsplit('.', 1)[-1] for app in available)
+
+ def unset_available_apps(self):
+ self.available_apps = None
+
class AppCache(BaseAppCache):
"""
@@ -315,6 +353,7 @@ class AppCache(BaseAppCache):
def __init__(self):
self.__dict__ = self.__shared_state
+
cache = AppCache()
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 0bfd1b38d3..f70750abed 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -22,6 +22,12 @@ class SQLCompiler(object):
self.connection = connection
self.using = using
self.quote_cache = {}
+ # When ordering a queryset with distinct on a column not part of the
+ # select set, the ordering column needs to be added to the select
+ # clause. This information is needed both in SQL construction and
+ # masking away the ordering selects from the returned row.
+ self.ordering_aliases = []
+ self.ordering_params = []
def pre_sql_setup(self):
"""
@@ -74,7 +80,7 @@ class SQLCompiler(object):
# another run of it.
self.refcounts_before = self.query.alias_refcount.copy()
out_cols, s_params = self.get_columns(with_col_aliases)
- ordering, ordering_group_by = self.get_ordering()
+ ordering, o_params, ordering_group_by = self.get_ordering()
distinct_fields = self.get_distinct()
@@ -95,9 +101,10 @@ class SQLCompiler(object):
if self.query.distinct:
result.append(self.connection.ops.distinct_sql(distinct_fields))
-
- result.append(', '.join(out_cols + self.query.ordering_aliases))
+ params.extend(o_params)
+ result.append(', '.join(out_cols + self.ordering_aliases))
params.extend(s_params)
+ params.extend(self.ordering_params)
result.append('FROM')
result.extend(from_)
@@ -319,7 +326,6 @@ class SQLCompiler(object):
result.append("%s.%s" % (qn(alias), qn2(col)))
return result
-
def get_ordering(self):
"""
Returns a tuple containing a list representing the SQL elements in the
@@ -357,7 +363,9 @@ class SQLCompiler(object):
# the table/column pairs we use and discard any after the first use.
processed_pairs = set()
- for field in ordering:
+ params = []
+ ordering_params = []
+ for pos, field in enumerate(ordering):
if field == '?':
result.append(self.connection.ops.random_function_sql())
continue
@@ -384,7 +392,7 @@ class SQLCompiler(object):
if not distinct or elt in select_aliases:
result.append('%s %s' % (elt, order))
group_by.append((elt, []))
- elif get_order_dir(field)[0] not in self.query.extra_select:
+ elif get_order_dir(field)[0] not in self.query.extra:
# 'col' is of the form 'field' or 'field1__field2' or
# '-field1__field2__field', etc.
for table, cols, order in self.find_ordering_name(field,
@@ -399,12 +407,19 @@ class SQLCompiler(object):
group_by.append((elt, []))
else:
elt = qn2(col)
- if distinct and col not in select_aliases:
- ordering_aliases.append(elt)
+ if col not in self.query.extra_select:
+ sql = "(%s) AS %s" % (self.query.extra[col][0], elt)
+ ordering_aliases.append(sql)
+ ordering_params.extend(self.query.extra[col][1])
+ else:
+ if distinct and col not in select_aliases:
+ ordering_aliases.append(elt)
+ ordering_params.extend(params)
result.append('%s %s' % (elt, order))
- group_by.append(self.query.extra_select[col])
- self.query.ordering_aliases = ordering_aliases
- return result, group_by
+ group_by.append(self.query.extra[col])
+ self.ordering_aliases = ordering_aliases
+ self.ordering_params = ordering_params
+ return result, params, group_by
def find_ordering_name(self, name, opts, alias=None, default_order='ASC',
already_seen=None):
@@ -631,12 +646,10 @@ class SQLCompiler(object):
if not select_related_descend(f, restricted, requested,
only_load.get(field_model)):
continue
- table = f.rel.to._meta.db_table
promote = nullable or f.null
- alias = self.query.join_parent_model(opts, model, root_alias, {})
- join_cols = f.get_joining_columns()
- alias = self.query.join((alias, table, join_cols),
- outer_if_first=promote, join_field=f)
+ _, _, _, joins, _ = self.query.setup_joins(
+ [f.name], opts, root_alias, outer_if_first=promote)
+ alias = joins[-1]
columns, aliases = self.get_default_columns(start_alias=alias,
opts=f.rel.to._meta, as_pairs=True)
self.query.related_select_cols.extend(
@@ -660,12 +673,9 @@ class SQLCompiler(object):
only_load.get(model), reverse=True):
continue
- alias = self.query.join_parent_model(opts, f.rel.to, root_alias, {})
- table = model._meta.db_table
- alias = self.query.join(
- (alias, table, f.get_joining_columns(reverse_join=True)),
- outer_if_first=True, join_field=f
- )
+ _, _, _, joins, _ = self.query.setup_joins(
+ [f.related_query_name()], opts, root_alias, outer_if_first=True)
+ alias = joins[-1]
from_parent = (opts.model if issubclass(model, opts.model)
else None)
columns, aliases = self.get_default_columns(start_alias=alias,
@@ -677,7 +687,7 @@ class SQLCompiler(object):
# Use True here because we are looking at the _reverse_ side of
# the relation, which is always nullable.
new_nullable = True
-
+ table = model._meta.db_table
self.fill_related_selections(model._meta, table, cur_depth+1,
next, restricted, new_nullable)
@@ -769,13 +779,13 @@ class SQLCompiler(object):
if not result_type:
return cursor
if result_type == SINGLE:
- if self.query.ordering_aliases:
- return cursor.fetchone()[:-len(self.query.ordering_aliases)]
+ if self.ordering_aliases:
+ return cursor.fetchone()[:-len(self.ordering_aliases)]
return cursor.fetchone()
# The MULTI case.
- if self.query.ordering_aliases:
- result = order_modified_iter(cursor, len(self.query.ordering_aliases),
+ if self.ordering_aliases:
+ result = order_modified_iter(cursor, len(self.ordering_aliases),
self.connection.features.empty_fetchmany_value)
else:
result = iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)),
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 154b6bd204..75e8e7540d 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -115,7 +115,6 @@ class Query(object):
self.default_cols = True
self.default_ordering = True
self.standard_ordering = True
- self.ordering_aliases = []
self.used_aliases = set()
self.filter_is_sticky = False
self.included_inherited_models = {}
@@ -227,7 +226,6 @@ class Query(object):
obj.default_ordering = self.default_ordering
obj.standard_ordering = self.standard_ordering
obj.included_inherited_models = self.included_inherited_models.copy()
- obj.ordering_aliases = []
obj.select = self.select[:]
obj.related_select_cols = []
obj.tables = self.tables[:]
@@ -926,10 +924,10 @@ class Query(object):
"""
if model in seen:
return seen[model]
- int_opts = opts
chain = opts.get_base_chain(model)
if chain is None:
return alias
+ curr_opts = opts
for int_model in chain:
if int_model in seen:
return seen[int_model]
@@ -937,14 +935,14 @@ class Query(object):
# with no parents, assign the new options
# object and skip to the next base in that
# case
- if not int_opts.parents[int_model]:
- int_opts = int_model._meta
+ if not curr_opts.parents[int_model]:
+ curr_opts = int_model._meta
continue
- link_field = int_opts.get_ancestor_link(int_model)
- int_opts = int_model._meta
- connection = (alias, int_opts.db_table, link_field.get_joining_columns())
- alias = seen[int_model] = self.join(connection, nullable=False,
- join_field=link_field)
+ link_field = curr_opts.get_ancestor_link(int_model)
+ _, _, _, joins, _ = self.setup_joins(
+ [link_field.name], curr_opts, alias)
+ curr_opts = int_model._meta
+ alias = seen[int_model] = joins[-1]
return alias or seen[None]
def remove_inherited_models(self):
@@ -1321,7 +1319,7 @@ class Query(object):
return path, final_field, targets
def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True,
- allow_explicit_fk=False):
+ allow_explicit_fk=False, outer_if_first=False):
"""
Compute the necessary table joins for the passage through the fields
given in 'names'. 'opts' is the Options class for the current model
@@ -1364,8 +1362,9 @@ class Query(object):
nullable = True
connection = alias, opts.db_table, join.join_field.get_joining_columns()
reuse = can_reuse if join.m2m else None
- alias = self.join(connection, reuse=reuse,
- nullable=nullable, join_field=join.join_field)
+ alias = self.join(
+ connection, reuse=reuse, nullable=nullable, join_field=join.join_field,
+ outer_if_first=outer_if_first)
joins.append(alias)
if hasattr(final_field, 'field'):
final_field = final_field.field
@@ -1913,5 +1912,7 @@ def alias_diff(refcounts_before, refcounts_after):
Given the before and after copies of refcounts works out which aliases
have been added to the after copy.
"""
+ # Use -1 as default value so that any join that is created, then trimmed
+ # is seen as added.
return set(t for t in refcounts_after
- if refcounts_after[t] > refcounts_before.get(t, 0))
+ if refcounts_after[t] > refcounts_before.get(t, -1))
diff --git a/django/forms/fields.py b/django/forms/fields.py
index ac68b9f1fc..52bcf9485c 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -125,7 +125,7 @@ class Field(object):
def validate(self, value):
if value in self.empty_values and self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
def run_validators(self, value):
if value in self.empty_values:
@@ -136,12 +136,8 @@ class Field(object):
v(value)
except ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages:
- message = self.error_messages[e.code]
- if e.params:
- message = message % e.params
- errors.append(message)
- else:
- errors.extend(e.messages)
+ e.message = self.error_messages[e.code]
+ errors.extend(e.error_list)
if errors:
raise ValidationError(errors)
@@ -250,7 +246,7 @@ class IntegerField(Field):
try:
value = int(str(value))
except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def widget_attrs(self, widget):
@@ -281,7 +277,7 @@ class FloatField(IntegerField):
try:
value = float(value)
except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def widget_attrs(self, widget):
@@ -327,7 +323,7 @@ class DecimalField(IntegerField):
try:
value = Decimal(value)
except DecimalException:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
def validate(self, value):
@@ -338,7 +334,7 @@ class DecimalField(IntegerField):
# since it is never equal to itself. However, NaN is the only value that
# isn't equal to itself, so we can use this to identify NaN
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
@@ -352,15 +348,24 @@ class DecimalField(IntegerField):
whole_digits = digits - decimals
if self.max_digits is not None and digits > self.max_digits:
- raise ValidationError(self.error_messages['max_digits'] % {
- 'max': self.max_digits})
+ raise ValidationError(
+ self.error_messages['max_digits'],
+ code='max_digits',
+ params={'max': self.max_digits},
+ )
if self.decimal_places is not None and decimals > self.decimal_places:
- raise ValidationError(self.error_messages['max_decimal_places'] % {
- 'max': self.decimal_places})
+ raise ValidationError(
+ self.error_messages['max_decimal_places'],
+ code='max_decimal_places',
+ params={'max': self.decimal_places},
+ )
if (self.max_digits is not None and self.decimal_places is not None
and whole_digits > (self.max_digits - self.decimal_places)):
- raise ValidationError(self.error_messages['max_whole_digits'] % {
- 'max': (self.max_digits - self.decimal_places)})
+ raise ValidationError(
+ self.error_messages['max_whole_digits'],
+ code='max_whole_digits',
+ params={'max': (self.max_digits - self.decimal_places)},
+ )
return value
def widget_attrs(self, widget):
@@ -395,7 +400,7 @@ class BaseTemporalField(Field):
return self.strptime(value, format)
except (ValueError, TypeError):
continue
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
def strptime(self, value, format):
raise NotImplementedError('Subclasses must define this method.')
@@ -475,7 +480,7 @@ class DateTimeField(BaseTemporalField):
# Input comes from a SplitDateTimeWidget, for example. So, it's two
# components: date and time.
if len(value) != 2:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if value[0] in self.empty_values and value[1] in self.empty_values:
return None
value = '%s %s' % tuple(value)
@@ -552,22 +557,22 @@ class FileField(Field):
file_name = data.name
file_size = data.size
except AttributeError:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if self.max_length is not None and len(file_name) > self.max_length:
- error_values = {'max': self.max_length, 'length': len(file_name)}
- raise ValidationError(self.error_messages['max_length'] % error_values)
+ params = {'max': self.max_length, 'length': len(file_name)}
+ raise ValidationError(self.error_messages['max_length'], code='max_length', params=params)
if not file_name:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
if not self.allow_empty_file and not file_size:
- raise ValidationError(self.error_messages['empty'])
+ raise ValidationError(self.error_messages['empty'], code='empty')
return data
def clean(self, data, initial=None):
# If the widget got contradictory inputs, we raise a validation error
if data is FILE_INPUT_CONTRADICTION:
- raise ValidationError(self.error_messages['contradiction'])
+ raise ValidationError(self.error_messages['contradiction'], code='contradiction')
# False means the field value should be cleared; further validation is
# not needed.
if data is False:
@@ -627,7 +632,10 @@ class ImageField(FileField):
Image.open(file).verify()
except Exception:
# Pillow (or PIL) doesn't recognize it as an image.
- six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2])
+ six.reraise(ValidationError, ValidationError(
+ self.error_messages['invalid_image'],
+ code='invalid_image',
+ ), sys.exc_info()[2])
if hasattr(f, 'seek') and callable(f.seek):
f.seek(0)
return f
@@ -638,10 +646,7 @@ class URLField(CharField):
default_error_messages = {
'invalid': _('Enter a valid URL.'),
}
-
- def __init__(self, max_length=None, min_length=None, *args, **kwargs):
- super(URLField, self).__init__(max_length, min_length, *args, **kwargs)
- self.validators.append(validators.URLValidator())
+ default_validators = [validators.URLValidator()]
def to_python(self, value):
@@ -655,7 +660,7 @@ class URLField(CharField):
except ValueError:
# urlparse.urlsplit can raise a ValueError with some
# misformatted URLs.
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
value = super(URLField, self).to_python(value)
if value:
@@ -699,7 +704,7 @@ class BooleanField(Field):
def validate(self, value):
if not value and self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the
@@ -783,7 +788,11 @@ class ChoiceField(Field):
"""
super(ChoiceField, self).validate(value)
if value and not self.valid_value(value):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
def valid_value(self, value):
"Check to see if the provided value is a valid choice"
@@ -817,7 +826,11 @@ class TypedChoiceField(ChoiceField):
try:
value = self.coerce(value)
except (ValueError, TypeError, ValidationError):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': value},
+ )
return value
@@ -833,7 +846,7 @@ class MultipleChoiceField(ChoiceField):
if not value:
return []
elif not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['invalid_list'])
+ raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
return [smart_text(val) for val in value]
def validate(self, value):
@@ -841,11 +854,15 @@ class MultipleChoiceField(ChoiceField):
Validates that the input is a list or tuple.
"""
if self.required and not value:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
# Validate that each value in the value list is in self.choices.
for val in value:
if not self.valid_value(val):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': val},
+ )
def _has_changed(self, initial, data):
if initial is None:
@@ -878,14 +895,18 @@ class TypedMultipleChoiceField(MultipleChoiceField):
try:
new_value.append(self.coerce(choice))
except (ValueError, TypeError, ValidationError):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': choice},
+ )
return new_value
def validate(self, value):
if value != self.empty_value:
super(TypedMultipleChoiceField, self).validate(value)
elif self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
class ComboField(Field):
@@ -959,25 +980,25 @@ class MultiValueField(Field):
if not value or isinstance(value, (list, tuple)):
if not value or not [v for v in value if v not in self.empty_values]:
if self.required:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
else:
return self.compress([])
else:
- raise ValidationError(self.error_messages['invalid'])
+ raise ValidationError(self.error_messages['invalid'], code='invalid')
for i, field in enumerate(self.fields):
try:
field_value = value[i]
except IndexError:
field_value = None
if self.required and field_value in self.empty_values:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
try:
clean_data.append(field.clean(field_value))
except ValidationError as e:
# Collect all validation errors in a single list, which we'll
# raise at the end of clean(), rather than raising a single
# exception for the first error we encounter.
- errors.extend(e.messages)
+ errors.extend(e.error_list)
if errors:
raise ValidationError(errors)
@@ -1085,9 +1106,9 @@ class SplitDateTimeField(MultiValueField):
# Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False).
if data_list[0] in self.empty_values:
- raise ValidationError(self.error_messages['invalid_date'])
+ raise ValidationError(self.error_messages['invalid_date'], code='invalid_date')
if data_list[1] in self.empty_values:
- raise ValidationError(self.error_messages['invalid_time'])
+ raise ValidationError(self.error_messages['invalid_time'], code='invalid_time')
result = datetime.datetime.combine(*data_list)
return from_current_timezone(result)
return None
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 0c598ac775..b25eeb30a4 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -170,11 +170,6 @@ class BaseForm(object):
if bf.label:
label = conditional_escape(force_text(bf.label))
- # Only add the suffix if the label does not end in
- # punctuation.
- if self.label_suffix:
- if label[-1] not in ':?.!':
- label = format_html('{0}{1}', label, self.label_suffix)
label = bf.label_tag(label) or ''
else:
label = ''
@@ -522,6 +517,9 @@ class BoundField(object):
If attrs are given, they're used as HTML attributes on the <label> tag.
"""
contents = contents or self.label
+ # Only add the suffix if the label does not end in punctuation.
+ if self.form.label_suffix and contents and contents[-1] not in ':?.!':
+ contents = format_html('{0}{1}', contents, self.form.label_suffix)
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
if id_:
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index fd98c43405..edd362c595 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -85,7 +85,10 @@ class BaseFormSet(object):
if self.is_bound:
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
if not form.is_valid():
- raise ValidationError('ManagementForm data is missing or has been tampered with')
+ raise ValidationError(
+ _('ManagementForm data is missing or has been tampered with'),
+ code='missing_management_form',
+ )
else:
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(),
@@ -263,6 +266,13 @@ class BaseFormSet(object):
self.full_clean()
return self._errors
+ def total_error_count(self):
+ """
+ Returns the number of errors across all forms in the formset.
+ """
+ return len(self.non_form_errors()) +\
+ sum(len(form_errors) for form_errors in self.errors)
+
def _should_delete_form(self, form):
"""
Returns whether or not the form was marked for deletion.
@@ -308,7 +318,9 @@ class BaseFormSet(object):
self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max:
raise ValidationError(ungettext(
"Please submit %d or fewer forms.",
- "Please submit %d or fewer forms.", self.max_num) % self.max_num)
+ "Please submit %d or fewer forms.", self.max_num) % self.max_num,
+ code='too_many_forms',
+ )
# Give self.clean() a chance to do cross-form validation.
self.clean()
except ValidationError as e:
diff --git a/django/forms/models.py b/django/forms/models.py
index 65434a6f6e..821f64199b 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -138,7 +138,9 @@ def model_to_dict(instance, fields=None, exclude=None):
data[f.name] = f.value_from_object(instance)
return data
-def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None):
+def fields_for_model(model, fields=None, exclude=None, widgets=None,
+ formfield_callback=None, localized_fields=None,
+ labels=None, help_texts=None, error_messages=None):
"""
Returns a ``SortedDict`` containing form fields for the given model.
@@ -149,7 +151,16 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
fields will be excluded from the returned fields, even if they are listed
in the ``fields`` argument.
- ``widgets`` is a dictionary of model field names mapped to a widget
+ ``widgets`` is a dictionary of model field names mapped to a widget.
+
+ ``localized_fields`` is a list of names of fields which should be localized.
+
+ ``labels`` is a dictionary of model field names mapped to a label.
+
+ ``help_texts`` is a dictionary of model field names mapped to a help text.
+
+ ``error_messages`` is a dictionary of model field names mapped to a
+ dictionary of error messages.
``formfield_callback`` is a callable that takes a model field and returns
a form field.
@@ -170,6 +181,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
kwargs['widget'] = widgets[f.name]
if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields):
kwargs['localize'] = True
+ if labels and f.name in labels:
+ kwargs['label'] = labels[f.name]
+ if help_texts and f.name in help_texts:
+ kwargs['help_text'] = help_texts[f.name]
+ if error_messages and f.name in error_messages:
+ kwargs['error_messages'] = error_messages[f.name]
if formfield_callback is None:
formfield = f.formfield(**kwargs)
@@ -197,6 +214,9 @@ class ModelFormOptions(object):
self.exclude = getattr(options, 'exclude', None)
self.widgets = getattr(options, 'widgets', None)
self.localized_fields = getattr(options, 'localized_fields', None)
+ self.labels = getattr(options, 'labels', None)
+ self.help_texts = getattr(options, 'help_texts', None)
+ self.error_messages = getattr(options, 'error_messages', None)
class ModelFormMetaclass(type):
@@ -248,7 +268,9 @@ class ModelFormMetaclass(type):
opts.fields = None
fields = fields_for_model(opts.model, opts.fields, opts.exclude,
- opts.widgets, formfield_callback, opts.localized_fields)
+ opts.widgets, formfield_callback,
+ opts.localized_fields, opts.labels,
+ opts.help_texts, opts.error_messages)
# make sure opts.fields doesn't specify an invalid field
none_model_fields = [k for k, v in six.iteritems(fields) if not v]
@@ -292,7 +314,17 @@ class BaseModelForm(BaseForm):
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted)
- def _update_errors(self, message_dict):
+ def _update_errors(self, errors):
+ for field, messages in errors.error_dict.items():
+ if field not in self.fields:
+ continue
+ field = self.fields[field]
+ for message in messages:
+ if isinstance(message, ValidationError):
+ if message.code in field.error_messages:
+ message.message = field.error_messages[message.code]
+
+ message_dict = errors.message_dict
for k, v in message_dict.items():
if k != NON_FIELD_ERRORS:
self._errors.setdefault(k, self.error_class()).extend(v)
@@ -367,17 +399,11 @@ class BaseModelForm(BaseForm):
if isinstance(field, InlineForeignKeyField):
exclude.append(f_name)
- # Clean the model instance's fields.
- try:
- self.instance.clean_fields(exclude=exclude)
- except ValidationError as e:
- self._update_errors(e.message_dict)
-
- # Call the model instance's clean method.
try:
- self.instance.clean()
+ self.instance.full_clean(exclude=exclude,
+ validate_unique=False)
except ValidationError as e:
- self._update_errors({NON_FIELD_ERRORS: e.messages})
+ self._update_errors(e)
# Validate uniqueness if needed.
if self._validate_unique:
@@ -392,7 +418,7 @@ class BaseModelForm(BaseForm):
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError as e:
- self._update_errors(e.message_dict)
+ self._update_errors(e)
def save(self, commit=True):
"""
@@ -416,7 +442,8 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
pass
def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
- formfield_callback=None, widgets=None, localized_fields=None):
+ formfield_callback=None, widgets=None, localized_fields=None,
+ labels=None, help_texts=None, error_messages=None):
"""
Returns a ModelForm containing form fields for the given model.
@@ -434,6 +461,13 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
``formfield_callback`` is a callable that takes a model field and returns
a form field.
+
+ ``labels`` is a dictionary of model field names mapped to a label.
+
+ ``help_texts`` is a dictionary of model field names mapped to a help text.
+
+ ``error_messages`` is a dictionary of model field names mapped to a
+ dictionary of error messages.
"""
# Create the inner Meta class. FIXME: ideally, we should be able to
# construct a ModelForm without creating and passing in a temporary
@@ -449,6 +483,12 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
attrs['widgets'] = widgets
if localized_fields is not None:
attrs['localized_fields'] = localized_fields
+ if labels is not None:
+ attrs['labels'] = labels
+ if help_texts is not None:
+ attrs['help_texts'] = help_texts
+ if error_messages is not None:
+ attrs['error_messages'] = error_messages
# If parent form class already has an inner Meta, the Meta we're
# creating needs to inherit from the parent's inner meta.
@@ -738,7 +778,8 @@ class BaseModelFormSet(BaseFormSet):
def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formset=BaseModelFormSet, extra=1, can_delete=False,
can_order=False, max_num=None, fields=None, exclude=None,
- widgets=None, validate_max=False, localized_fields=None):
+ widgets=None, validate_max=False, localized_fields=None,
+ labels=None, help_texts=None, error_messages=None):
"""
Returns a FormSet class for the given Django model class.
"""
@@ -759,7 +800,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None,
form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
formfield_callback=formfield_callback,
- widgets=widgets, localized_fields=localized_fields)
+ widgets=widgets, localized_fields=localized_fields,
+ labels=labels, help_texts=help_texts, error_messages=error_messages)
FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
can_order=can_order, can_delete=can_delete,
validate_max=validate_max)
@@ -898,7 +940,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
formset=BaseInlineFormSet, fk_name=None,
fields=None, exclude=None, extra=3, can_order=False,
can_delete=True, max_num=None, formfield_callback=None,
- widgets=None, validate_max=False, localized_fields=None):
+ widgets=None, validate_max=False, localized_fields=None,
+ labels=None, help_texts=None, error_messages=None):
"""
Returns an ``InlineFormSet`` for the given kwargs.
@@ -922,6 +965,9 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'widgets': widgets,
'validate_max': validate_max,
'localized_fields': localized_fields,
+ 'labels': labels,
+ 'help_texts': help_texts,
+ 'error_messages': error_messages,
}
FormSet = modelformset_factory(model, **kwargs)
FormSet.fk = fk
@@ -964,7 +1010,7 @@ class InlineForeignKeyField(Field):
else:
orig = self.parent_instance.pk
if force_text(value) != force_text(orig):
- raise ValidationError(self.error_messages['invalid_choice'])
+ raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return self.parent_instance
def _has_changed(self, initial, data):
@@ -1079,7 +1125,7 @@ class ModelChoiceField(ChoiceField):
key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value})
except (ValueError, self.queryset.model.DoesNotExist):
- raise ValidationError(self.error_messages['invalid_choice'])
+ raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
return value
def validate(self, value):
@@ -1114,22 +1160,30 @@ class ModelMultipleChoiceField(ModelChoiceField):
def clean(self, value):
if self.required and not value:
- raise ValidationError(self.error_messages['required'])
+ raise ValidationError(self.error_messages['required'], code='required')
elif not self.required and not value:
return self.queryset.none()
if not isinstance(value, (list, tuple)):
- raise ValidationError(self.error_messages['list'])
+ raise ValidationError(self.error_messages['list'], code='list')
key = self.to_field_name or 'pk'
for pk in value:
try:
self.queryset.filter(**{key: pk})
except ValueError:
- raise ValidationError(self.error_messages['invalid_pk_value'] % {'pk': pk})
+ raise ValidationError(
+ self.error_messages['invalid_pk_value'],
+ code='invalid_pk_value',
+ params={'pk': pk},
+ )
qs = self.queryset.filter(**{'%s__in' % key: value})
pks = set([force_text(getattr(o, key)) for o in qs])
for val in value:
if force_text(val) not in pks:
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
+ raise ValidationError(
+ self.error_messages['invalid_choice'],
+ code='invalid_choice',
+ params={'value': val},
+ )
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(value)
diff --git a/django/forms/util.py b/django/forms/util.py
index f1b864e6b3..568cdd1086 100644
--- a/django/forms/util.py
+++ b/django/forms/util.py
@@ -80,12 +80,17 @@ def from_current_timezone(value):
try:
return timezone.make_aware(value, current_timezone)
except Exception:
- msg = _(
+ message = _(
'%(datetime)s couldn\'t be interpreted '
'in time zone %(current_timezone)s; it '
- 'may be ambiguous or it may not exist.') % {'datetime': value, 'current_timezone':
- current_timezone}
- six.reraise(ValidationError, ValidationError(msg), sys.exc_info()[2])
+ 'may be ambiguous or it may not exist.'
+ )
+ params = {'datetime': value, 'current_timezone': current_timezone}
+ six.reraise(ValidationError, ValidationError(
+ message,
+ code='ambiguous_timezone',
+ params=params,
+ ), sys.exc_info()[2])
return value
def to_current_timezone(value):
diff --git a/django/http/response.py b/django/http/response.py
index 9aa49b1d5f..784f21174e 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -66,6 +66,9 @@ REASON_PHRASES = {
423: 'LOCKED',
424: 'FAILED DEPENDENCY',
426: 'UPGRADE REQUIRED',
+ 428: 'PRECONDITION REQUIRED',
+ 429: 'TOO MANY REQUESTS',
+ 431: 'REQUEST HEADER FIELDS TOO LARGE',
500: 'INTERNAL SERVER ERROR',
501: 'NOT IMPLEMENTED',
502: 'BAD GATEWAY',
@@ -76,6 +79,7 @@ REASON_PHRASES = {
507: 'INSUFFICIENT STORAGE',
508: 'LOOP DETECTED',
510: 'NOT EXTENDED',
+ 511: 'NETWORK AUTHENTICATION REQUIRED',
}
diff --git a/django/test/client.py b/django/test/client.py
index 2ed0df8fea..94cfada725 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -272,7 +272,6 @@ class RequestFactory(object):
parsed = urlparse(path)
r = {
- 'CONTENT_TYPE': str('text/html; charset=utf-8'),
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
'REQUEST_METHOD': str('GET'),
@@ -303,7 +302,6 @@ class RequestFactory(object):
parsed = urlparse(path)
r = {
- 'CONTENT_TYPE': str('text/html; charset=utf-8'),
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
'REQUEST_METHOD': str('HEAD'),
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 08c03154b1..984d21ce18 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -24,10 +24,12 @@ from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler
from django.core.management import call_command
from django.core.management.color import no_style
+from django.core.management.commands import flush
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException)
from django.core.urlresolvers import clear_url_caches, set_urlconf
from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction
+from django.db.models.loading import cache
from django.forms.fields import CharField
from django.http import QueryDict
from django.test.client import Client
@@ -195,9 +197,9 @@ class SimpleTestCase(ut2.TestCase):
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
- * If the Test Case class has a 'urls' member, replace the
- ROOT_URLCONF with it.
- * Clearing the mail test outbox.
+ * Creating a test client.
+ * If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
+ * Clearing the mail test outbox.
"""
self.client = self.client_class()
self._urlconf_setup()
@@ -211,6 +213,10 @@ class SimpleTestCase(ut2.TestCase):
clear_url_caches()
def _post_teardown(self):
+ """Performs any post-test things. This includes:
+
+ * Putting back the original ROOT_URLCONF if it was changed.
+ """
self._urlconf_teardown()
def _urlconf_teardown(self):
@@ -725,15 +731,29 @@ class TransactionTestCase(SimpleTestCase):
# test case
reset_sequences = False
+ # Subclasses can enable only a subset of apps for faster tests
+ available_apps = None
+
def _pre_setup(self):
"""Performs any pre-test setup. This includes:
- * Flushing the database.
- * If the Test Case class has a 'fixtures' member, installing the
- named fixtures.
+ * If the class has an 'available_apps' attribute, restricting the app
+ cache to these applications, then firing post_syncdb -- it must run
+ with the correct set of applications for the test case.
+ * If the class has a 'fixtures' attribute, installing these fixtures.
"""
super(TransactionTestCase, self)._pre_setup()
- self._fixture_setup()
+ if self.available_apps is not None:
+ cache.set_available_apps(self.available_apps)
+ for db_name in self._databases_names(include_mirrors=False):
+ flush.Command.emit_post_syncdb(
+ verbosity=0, interactive=False, database=db_name)
+ try:
+ self._fixture_setup()
+ except Exception:
+ if self.available_apps is not None:
+ cache.unset_available_apps()
+ raise
def _databases_names(self, include_mirrors=True):
# If the test case has a multi_db=True flag, act on all databases,
@@ -771,26 +791,33 @@ class TransactionTestCase(SimpleTestCase):
def _post_teardown(self):
"""Performs any post-test things. This includes:
- * Putting back the original ROOT_URLCONF if it was changed.
- * Force closing the connection, so that the next test gets
- a clean cursor.
+ * Flushing the contents of the database, to leave a clean slate. If
+ the class has an 'available_apps' attribute, post_syncdb isn't fired.
+ * Force-closing the connection, so the next test gets a clean cursor.
"""
- self._fixture_teardown()
- super(TransactionTestCase, self)._post_teardown()
- # Some DB cursors include SQL statements as part of cursor
- # creation. If you have a test that does rollback, the effect
- # of these statements is lost, which can effect the operation
- # of tests (e.g., losing a timezone setting causing objects to
- # be created with the wrong time).
- # To make sure this doesn't happen, get a clean connection at the
- # start of every test.
- for conn in connections.all():
- conn.close()
+ try:
+ self._fixture_teardown()
+ super(TransactionTestCase, self)._post_teardown()
+ # Some DB cursors include SQL statements as part of cursor
+ # creation. If you have a test that does rollback, the effect of
+ # these statements is lost, which can effect the operation of
+ # tests (e.g., losing a timezone setting causing objects to be
+ # created with the wrong time). To make sure this doesn't happen,
+ # get a clean connection at the start of every test.
+ for conn in connections.all():
+ conn.close()
+ finally:
+ cache.unset_available_apps()
def _fixture_teardown(self):
+ # Allow TRUNCATE ... CASCADE and don't emit the post_syncdb signal
+ # when flushing only a subset of the apps
for db_name in self._databases_names(include_mirrors=False):
- call_command('flush', verbosity=0, interactive=False, database=db_name,
- skip_validation=True, reset_sequences=False)
+ call_command('flush', verbosity=0, interactive=False,
+ database=db_name, skip_validation=True,
+ reset_sequences=False,
+ allow_cascade=self.available_apps is not None,
+ inhibit_post_syncdb=self.available_apps is not None)
def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True):
items = six.moves.map(transform, qs)
diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py
index eaacfb4623..4d5352272b 100644
--- a/django/utils/ipv6.py
+++ b/django/utils/ipv6.py
@@ -2,10 +2,11 @@
# Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/
# Licensed under the Apache License, Version 2.0 (the "License").
from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
from django.utils.six.moves import xrange
def clean_ipv6_address(ip_str, unpack_ipv4=False,
- error_message="This is not a valid IPv6 address."):
+ error_message=_("This is not a valid IPv6 address.")):
"""
Cleans a IPv6 address string.
@@ -31,7 +32,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False,
doublecolon_len = 0
if not is_valid_ipv6_address(ip_str):
- raise ValidationError(error_message)
+ raise ValidationError(error_message, code='invalid')
# This algorithm can only handle fully exploded
# IP strings
diff --git a/django/views/generic/base.py b/django/views/generic/base.py
index 286a18d0f2..2487ccba52 100644
--- a/django/views/generic/base.py
+++ b/django/views/generic/base.py
@@ -5,6 +5,7 @@ from functools import update_wrapper
from django import http
from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import reverse, NoReverseMatch
from django.template.response import TemplateResponse
from django.utils.decorators import classonlymethod
from django.utils import six
@@ -160,9 +161,10 @@ class RedirectView(View):
"""
permanent = True
url = None
+ pattern_name = None
query_string = False
- def get_redirect_url(self, **kwargs):
+ def get_redirect_url(self, *args, **kwargs):
"""
Return the URL redirect to. Keyword arguments from the
URL pattern match generating the redirect request
@@ -170,15 +172,21 @@ class RedirectView(View):
"""
if self.url:
url = self.url % kwargs
- args = self.request.META.get('QUERY_STRING', '')
- if args and self.query_string:
- url = "%s?%s" % (url, args)
- return url
+ elif self.pattern_name:
+ try:
+ url = reverse(self.pattern_name, args=args, kwargs=kwargs)
+ except NoReverseMatch:
+ return None
else:
return None
+ args = self.request.META.get('QUERY_STRING', '')
+ if args and self.query_string:
+ url = "%s?%s" % (url, args)
+ return url
+
def get(self, request, *args, **kwargs):
- url = self.get_redirect_url(**kwargs)
+ url = self.get_redirect_url(*args, **kwargs)
if url:
if self.permanent:
return http.HttpResponsePermanentRedirect(url)
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
index cf87aeed27..b31d7a218f 100644
--- a/django/views/generic/edit.py
+++ b/django/views/generic/edit.py
@@ -242,8 +242,8 @@ class DeletionMixin(object):
return HttpResponseRedirect(success_url)
# Add support for browsers which only accept GET and POST for now.
- def post(self, *args, **kwargs):
- return self.delete(*args, **kwargs)
+ def post(self, request, *args, **kwargs):
+ return self.delete(request, *args, **kwargs)
def get_success_url(self):
if self.success_url: