1
0
mirror of https://github.com/django/django.git synced 2025-06-04 19:19:13 +00:00

Merge remote-tracking branch 'core/master' into schema-alteration

Conflicts:
	django/db/models/loading.py
This commit is contained in:
Andrew Godwin 2013-06-19 12:03:20 +01:00
commit 9daf81b94e
161 changed files with 3128 additions and 1220 deletions

1
.gitignore vendored
View File

@ -4,5 +4,6 @@
MANIFEST MANIFEST
dist/ dist/
docs/_build/ docs/_build/
docs/locale/
tests/coverage_html/ tests/coverage_html/
tests/.coverage tests/.coverage

View File

@ -45,6 +45,7 @@ The PRIMARY AUTHORS are (and/or have been):
* Donald Stufft * Donald Stufft
* Daniel Lindsley * Daniel Lindsley
* Marc Tamlyn * Marc Tamlyn
* Baptiste Mispelon
More information on the main contributors to Django can be found in More information on the main contributors to Django can be found in
docs/internals/committers.txt. docs/internals/committers.txt.
@ -413,7 +414,6 @@ answer newbie questions, and generally made Django that much better:
Slawek Mikula <slawek dot mikula at gmail dot com> Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com> Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com> Shawn Milochik <shawn@milochik.com>
Baptiste Mispelon <bmispelon@gmail.com>
mitakummaa@gmail.com mitakummaa@gmail.com
Taylor Mitchell <taylor.mitchell@gmail.com> Taylor Mitchell <taylor.mitchell@gmail.com>
mmarshall mmarshall
@ -550,6 +550,7 @@ answer newbie questions, and generally made Django that much better:
Thomas Steinacher <http://www.eggdrop.ch/> Thomas Steinacher <http://www.eggdrop.ch/>
Emil Stenström <em@kth.se> Emil Stenström <em@kth.se>
Johan C. Stöver <johan@nilling.nl> Johan C. Stöver <johan@nilling.nl>
Chris Streeter <chris@chrisstreeter.com>
Nowell Strite <http://nowell.strite.org/> Nowell Strite <http://nowell.strite.org/>
Thomas Stromberg <tstromberg@google.com> Thomas Stromberg <tstromberg@google.com>
Hannes Struß <x@hannesstruss.de> Hannes Struß <x@hannesstruss.de>

View File

@ -22,15 +22,12 @@ class AdminAuthenticationForm(AuthenticationForm):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password') password = self.cleaned_data.get('password')
message = ERROR_MESSAGE message = ERROR_MESSAGE
params = {'username': self.username_field.verbose_name}
if username and password: if username and password:
self.user_cache = authenticate(username=username, password=password) self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None: if self.user_cache is None:
raise forms.ValidationError(message % { raise forms.ValidationError(message, code='invalid', params=params)
'username': self.username_field.verbose_name
})
elif not self.user_cache.is_active or not self.user_cache.is_staff: elif not self.user_cache.is_active or not self.user_cache.is_staff:
raise forms.ValidationError(message % { raise forms.ValidationError(message, code='invalid', params=params)
'username': self.username_field.verbose_name
})
return self.cleaned_data return self.cleaned_data

View File

@ -13,6 +13,7 @@ from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_o
model_format_dict, NestedObjects, lookup_needs_distinct) model_format_dict, NestedObjects, lookup_needs_distinct)
from django.contrib.admin import validation from django.contrib.admin import validation
from django.contrib.admin.templatetags.admin_static import static 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.contrib import messages
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError, FieldError 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.safestring import mark_safe
from django.utils import six from django.utils import six
from django.utils.deprecation import RenameMethodsBase from django.utils.deprecation import RenameMethodsBase
from django.utils.http import urlencode
from django.utils.text import capfirst, get_text_list from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext from django.utils.translation import ungettext
@ -393,6 +395,7 @@ class ModelAdmin(BaseModelAdmin):
save_as = False save_as = False
save_on_top = False save_on_top = False
paginator = Paginator paginator = Paginator
preserve_filters = True
inlines = [] inlines = []
# Custom templates (designed to be over-ridden in subclasses) # Custom templates (designed to be over-ridden in subclasses)
@ -755,6 +758,27 @@ class ModelAdmin(BaseModelAdmin):
""" """
return self.list_filter 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): def construct_change_message(self, request, form, formsets):
""" """
Construct a change message from a changed object. 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): def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta opts = self.model._meta
app_label = opts.app_label 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({ context.update({
'add': add, 'add': add,
'change': change, 'change': change,
@ -877,11 +903,19 @@ class ModelAdmin(BaseModelAdmin):
""" """
opts = obj._meta opts = obj._meta
pk_value = obj._get_pk_val() 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)} msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
# Here, we distinguish between different save types by checking for # Here, we distinguish between different save types by checking for
# the presence of keys in request.POST. # 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 msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS) self.message_user(request, msg, messages.SUCCESS)
if post_url_continue is None: if post_url_continue is None:
@ -889,20 +923,16 @@ class ModelAdmin(BaseModelAdmin):
(opts.app_label, opts.model_name), (opts.app_label, opts.model_name),
args=(pk_value,), args=(pk_value,),
current_app=self.admin_site.name) current_app=self.admin_site.name)
if "_popup" in request.POST: post_url_continue = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue)
post_url_continue += "?_popup=1"
return HttpResponseRedirect(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: elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict 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) 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: else:
msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS) self.message_user(request, msg, messages.SUCCESS)
@ -913,30 +943,36 @@ class ModelAdmin(BaseModelAdmin):
Determines the HttpResponse for the change_view stage. Determines the HttpResponse for the change_view stage.
""" """
opts = self.model._meta opts = self.model._meta
pk_value = obj._get_pk_val() 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)} msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
if "_continue" in request.POST: if "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS) self.message_user(request, msg, messages.SUCCESS)
if "_popup" in request.REQUEST: redirect_url = request.path
return HttpResponseRedirect(request.path + "?_popup=1") redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
else: return HttpResponseRedirect(redirect_url)
return HttpResponseRedirect(request.path)
elif "_saveasnew" in request.POST: elif "_saveasnew" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS) self.message_user(request, msg, messages.SUCCESS)
return HttpResponseRedirect(reverse('admin:%s_%s_change' % redirect_url = reverse('admin:%s_%s_change' %
(opts.app_label, opts.model_name), (opts.app_label, opts.model_name),
args=(pk_value,), args=(pk_value,),
current_app=self.admin_site.name)) 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: elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict 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) self.message_user(request, msg, messages.SUCCESS)
return HttpResponseRedirect(reverse('admin:%s_%s_add' % redirect_url = reverse('admin:%s_%s_add' %
(opts.app_label, opts.model_name), (opts.app_label, opts.model_name),
current_app=self.admin_site.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: else:
msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS) self.message_user(request, msg, messages.SUCCESS)
@ -952,6 +988,8 @@ class ModelAdmin(BaseModelAdmin):
post_url = reverse('admin:%s_%s_changelist' % post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name), (opts.app_label, opts.model_name),
current_app=self.admin_site.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: else:
post_url = reverse('admin:index', post_url = reverse('admin:index',
current_app=self.admin_site.name) current_app=self.admin_site.name)
@ -963,10 +1001,13 @@ class ModelAdmin(BaseModelAdmin):
when editing an existing object. when editing an existing object.
""" """
opts = self.model._meta opts = self.model._meta
if self.has_change_permission(request, None): if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' % post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name), (opts.app_label, opts.model_name),
current_app=self.admin_site.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: else:
post_url = reverse('admin:index', post_url = reverse('admin:index',
current_app=self.admin_site.name) current_app=self.admin_site.name)
@ -1122,6 +1163,7 @@ class ModelAdmin(BaseModelAdmin):
'inline_admin_formsets': inline_admin_formsets, 'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets), 'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label, 'app_label': opts.app_label,
'preserved_filters': self.get_preserved_filters(request),
} }
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True) 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, 'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets), 'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label, 'app_label': opts.app_label,
'preserved_filters': self.get_preserved_filters(request),
} }
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url) return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
@ -1357,11 +1400,13 @@ class ModelAdmin(BaseModelAdmin):
'cl': cl, 'cl': cl,
'media': media, 'media': media,
'has_add_permission': self.has_add_permission(request), 'has_add_permission': self.has_add_permission(request),
'opts': cl.opts,
'app_label': app_label, 'app_label': app_label,
'action_form': action_form, 'action_form': action_form,
'actions_on_top': self.actions_on_top, 'actions_on_top': self.actions_on_top,
'actions_on_bottom': self.actions_on_bottom, 'actions_on_bottom': self.actions_on_bottom,
'actions_selection_counter': self.actions_selection_counter, 'actions_selection_counter': self.actions_selection_counter,
'preserved_filters': self.get_preserved_filters(request),
} }
context.update(extra_context or {}) context.update(extra_context or {})
@ -1406,12 +1451,16 @@ class ModelAdmin(BaseModelAdmin):
'obj': force_text(obj_display)}, 'obj': force_text(obj_display)},
messages.SUCCESS) messages.SUCCESS)
if not self.has_change_permission(request, None): if self.has_change_permission(request, None):
return HttpResponseRedirect(reverse('admin:index', post_url = reverse('admin:%s_%s_changelist' %
current_app=self.admin_site.name)) (opts.app_label, opts.model_name),
return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % current_app=self.admin_site.name)
(opts.app_label, opts.model_name), preserved_filters = self.get_preserved_filters(request)
current_app=self.admin_site.name)) 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) object_name = force_text(opts.verbose_name)
@ -1429,6 +1478,7 @@ class ModelAdmin(BaseModelAdmin):
"protected": protected, "protected": protected,
"opts": opts, "opts": opts,
"app_label": app_label, "app_label": app_label,
'preserved_filters': self.get_preserved_filters(request),
} }
context.update(extra_context or {}) context.update(extra_context or {})
@ -1463,6 +1513,7 @@ class ModelAdmin(BaseModelAdmin):
'object': obj, 'object': obj,
'app_label': app_label, 'app_label': app_label,
'opts': opts, 'opts': opts,
'preserved_filters': self.get_preserved_filters(request),
} }
context.update(extra_context or {}) context.update(extra_context or {})
return TemplateResponse(request, self.object_history_template or [ return TemplateResponse(request, self.object_history_template or [
@ -1574,13 +1625,13 @@ class InlineModelAdmin(BaseModelAdmin):
'class_name': p._meta.verbose_name, 'class_name': p._meta.verbose_name,
'instance': p} 'instance': p}
) )
msg_dict = {'class_name': self._meta.model._meta.verbose_name, params = {'class_name': self._meta.model._meta.verbose_name,
'instance': self.instance, 'instance': self.instance,
'related_objects': get_text_list(objs, _('and'))} 'related_objects': get_text_list(objs, _('and'))}
msg = _("Deleting %(class_name)s %(instance)s would require " msg = _("Deleting %(class_name)s %(instance)s would require "
"deleting the following protected related objects: " "deleting the following protected related objects: "
"%(related_objects)s") % msg_dict "%(related_objects)s")
raise ValidationError(msg) raise ValidationError(msg, code='deleting_protected', params=params)
def is_valid(self): def is_valid(self):
result = super(DeleteProtectedModelForm, self).is_valid() result = super(DeleteProtectedModelForm, self).is_valid()

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n admin_static admin_modify %} {% load i18n admin_urls admin_static admin_modify %}
{% load admin_urls %}
{% block extrahead %}{{ block.super }} {% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script> <script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
@ -29,7 +28,10 @@
{% if change %}{% if not is_popup %} {% if change %}{% if not is_popup %}
<ul class="object-tools"> <ul class="object-tools">
{% block object-tools-items %} {% 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%} {% 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 %} {% endblock %}
</ul> </ul>

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n admin_static admin_list %} {% load i18n admin_urls admin_static admin_list %}
{% load admin_urls %}
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {{ block.super }}
@ -54,7 +53,8 @@
<ul class="object-tools"> <ul class="object-tools">
{% block object-tools-items %} {% block object-tools-items %}
<li> <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 %} {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a> </a>
</li> </li>
@ -64,7 +64,7 @@
{% endblock %} {% endblock %}
{% if cl.formset.errors %} {% if cl.formset.errors %}
<p class="errornote"> <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> </p>
{{ cl.formset.non_form_errors }} {{ cl.formset.non_form_errors }}
{% endif %} {% endif %}

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n %} {% load i18n admin_urls %}
{% load admin_urls %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n l10n %} {% load i18n l10n admin_urls %}
{% load admin_urls %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %} {% extends "admin/base_site.html" %}
{% load i18n %} {% load i18n admin_urls %}
{% load admin_urls %}
{% block breadcrumbs %} {% block breadcrumbs %}
<div class="breadcrumbs"> <div class="breadcrumbs">

View File

@ -1,7 +1,10 @@
{% load i18n admin_urls %} {% load i18n admin_urls %}
<div class="submit-row"> <div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %} {% 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_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_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 %} {% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime import datetime
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.util import (lookup_field, display_for_field, from django.contrib.admin.util import (lookup_field, display_for_field,
display_for_value, label_for_field) display_for_value, label_for_field)
from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE, 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] table_tag = {True:'th', False:'td'}[first]
first = False first = False
url = cl.url_for_result(result) 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. # Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings. # Problem cases are long ints (23L) and non-ASCII strings.
if cl.to_field: if cl.to_field:

View File

@ -37,7 +37,8 @@ def submit_row(context):
not is_popup and (not save_as or context['add']), not is_popup and (not save_as or context['add']),
'show_save_and_continue': not is_popup and context['has_change_permission'], 'show_save_and_continue': not is_popup and context['has_change_permission'],
'is_popup': is_popup, 'is_popup': is_popup,
'show_save': True 'show_save': True,
'preserved_filters': context.get('preserved_filters'),
} }
if context.get('original') is not None: if context.get('original') is not None:
ctx['original'] = context['original'] ctx['original'] = context['original']

View File

@ -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 import template
from django.contrib.admin.util import quote from django.contrib.admin.util import quote
from django.core.urlresolvers import resolve, Resolver404
register = template.Library() register = template.Library()
@register.filter @register.filter
def admin_urlname(value, arg): def admin_urlname(value, arg):
return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg) return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg)
@ -11,3 +20,36 @@ def admin_urlname(value, arg):
@register.filter @register.filter
def admin_urlquote(value): def admin_urlquote(value):
return quote(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)

View File

@ -5,7 +5,16 @@ from django.utils.module_loading import import_by_path
from django.utils.unittest import SkipTest from django.utils.unittest import SkipTest
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
class AdminSeleniumWebDriverTestCase(LiveServerTestCase): 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' webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
@classmethod @classmethod

View File

@ -59,6 +59,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
self.list_per_page = list_per_page self.list_per_page = list_per_page
self.list_max_show_all = list_max_show_all self.list_max_show_all = list_max_show_all
self.model_admin = model_admin self.model_admin = model_admin
self.preserved_filters = model_admin.get_preserved_filters(request)
# Get search parameters from the query string. # Get search parameters from the query string.
try: try:

View File

@ -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 import authenticate, get_user_model
from django.contrib.auth.models import User 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.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site from django.contrib.sites.models import get_current_site
@ -29,7 +29,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
encoded = value encoded = value
final_attrs = self.build_attrs(attrs) 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.")) summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
else: else:
try: try:
@ -97,14 +97,19 @@ class UserCreationForm(forms.ModelForm):
User._default_manager.get(username=username) User._default_manager.get(username=username)
except User.DoesNotExist: except User.DoesNotExist:
return username 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): def clean_password2(self):
password1 = self.cleaned_data.get("password1") password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2") password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2: if password1 and password2 and password1 != password2:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['password_mismatch']) self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2 return password2
def save(self, commit=True): def save(self, commit=True):
@ -183,11 +188,15 @@ class AuthenticationForm(forms.Form):
password=password) password=password)
if self.user_cache is None: if self.user_cache is None:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['invalid_login'] % { self.error_messages['invalid_login'],
'username': self.username_field.verbose_name code='invalid_login',
}) params={'username': self.username_field.verbose_name},
)
elif not self.user_cache.is_active: 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 return self.cleaned_data
def check_for_test_cookie(self): def check_for_test_cookie(self):
@ -222,7 +231,7 @@ class PasswordResetForm(forms.Form):
for user in users: for user in users:
# Make sure that no email is sent to a user that actually has # Make sure that no email is sent to a user that actually has
# a password marked as unusable # a password marked as unusable
if user.password == UNUSABLE_PASSWORD: if not user.has_usable_password():
continue continue
if not domain_override: if not domain_override:
current_site = get_current_site(request) current_site = get_current_site(request)
@ -269,7 +278,9 @@ class SetPasswordForm(forms.Form):
if password1 and password2: if password1 and password2:
if password1 != password2: if password1 != password2:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['password_mismatch']) self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2 return password2
def save(self, commit=True): def save(self, commit=True):
@ -298,7 +309,9 @@ class PasswordChangeForm(SetPasswordForm):
old_password = self.cleaned_data["old_password"] old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password): if not self.user.check_password(old_password):
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['password_incorrect']) self.error_messages['password_incorrect'],
code='password_incorrect',
)
return old_password return old_password
PasswordChangeForm.base_fields = SortedDict([ PasswordChangeForm.base_fields = SortedDict([
@ -329,7 +342,9 @@ class AdminPasswordChangeForm(forms.Form):
if password1 and password2: if password1 and password2:
if password1 != password2: if password1 != password2:
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['password_mismatch']) self.error_messages['password_mismatch'],
code='password_mismatch',
)
return password2 return password2
def save(self, commit=True): def save(self, commit=True):

View File

@ -17,7 +17,8 @@ from django.utils.module_loading import import_by_path
from django.utils.translation import ugettext_noop as _ 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 HASHERS = None # lazily loaded from PASSWORD_HASHERS
PREFERRED_HASHER = None # defaults to first item in 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): 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 return False
try: try:
hasher = identify_hasher(encoded) 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 If setter is specified, it'll be called when you need to
regenerate the password. regenerate the password.
""" """
if not password or not is_password_usable(encoded): if not is_password_usable(encoded):
return False return False
preferred = get_hasher(preferred) 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 Turn a plain-text password into a hash for database storage
Same as encode() but generates a new random salt. If Same as encode() but generates a new random salt.
password is None or blank then UNUSABLE_PASSWORD will be If password is None then a concatenation of
returned which disallows logins. 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: if password is None:
return UNUSABLE_PASSWORD return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher) hasher = get_hasher(hasher)
if not salt: if not salt:
@ -171,12 +174,12 @@ class BasePasswordHasher(object):
name = mod_path = self.library name = mod_path = self.library
try: try:
module = importlib.import_module(mod_path) module = importlib.import_module(mod_path)
except ImportError: except ImportError as e:
raise ValueError("Couldn't load %s password algorithm " raise ValueError("Couldn't load %r algorithm library: %s" %
"library" % name) (self.__class__.__name__, e))
return module return module
raise ValueError("Hasher '%s' doesn't specify a library attribute" % raise ValueError("Hasher %r doesn't specify a library attribute" %
self.__class__) self.__class__.__name__)
def salt(self): def salt(self):
""" """
@ -222,7 +225,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
digest = hashlib.sha256 digest = hashlib.sha256
def encode(self, password, salt, iterations=None): def encode(self, password, salt, iterations=None):
assert password assert password is not None
assert salt and '$' not in salt assert salt and '$' not in salt
if not iterations: if not iterations:
iterations = self.iterations iterations = self.iterations
@ -350,7 +353,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
algorithm = "sha1" algorithm = "sha1"
def encode(self, password, salt): def encode(self, password, salt):
assert password assert password is not None
assert salt and '$' not in salt assert salt and '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest() hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash) return "%s$%s$%s" % (self.algorithm, salt, hash)
@ -378,7 +381,7 @@ class MD5PasswordHasher(BasePasswordHasher):
algorithm = "md5" algorithm = "md5"
def encode(self, password, salt): def encode(self, password, salt):
assert password assert password is not None
assert salt and '$' not in salt assert salt and '$' not in salt
hash = hashlib.md5(force_bytes(salt + password)).hexdigest() hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash) return "%s$%s$%s" % (self.algorithm, salt, hash)

View File

@ -11,7 +11,7 @@ from django.contrib.auth import models as auth_app, get_user_model
from django.core import exceptions from django.core import exceptions
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.db import DEFAULT_DB_ALIAS, router 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.encoding import DEFAULT_LOCALE_ENCODING
from django.utils import six from django.utils import six
from django.utils.six.moves import input from django.utils.six.moves import input
@ -60,6 +60,11 @@ def _check_permission_clashing(custom, builtin, ctype):
pool.add(codename) pool.add(codename)
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs): 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): if not router.allow_syncdb(db, auth_app.Permission):
return 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): 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): if UserModel in created_models and kwargs.get('interactive', True):
msg = ("\nYou just installed Django's auth system, which means you " msg = ("\nYou just installed Django's auth system, which means you "

View File

@ -16,7 +16,7 @@ from django.utils import timezone
from django.contrib import auth from django.contrib import auth
# UNUSABLE_PASSWORD is still imported here for backwards compatibility # UNUSABLE_PASSWORD is still imported here for backwards compatibility
from django.contrib.auth.hashers import ( 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.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible

View File

@ -156,6 +156,18 @@ class CustomUserNonUniqueUsername(AbstractBaseUser):
app_label = 'auth' 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): class CustomUserBadRequiredFields(AbstractBaseUser):
"A user with a non-unique username" "A user with a non-unique username"
username = models.CharField(max_length=30, unique=True) username = models.CharField(max_length=30, unique=True)

View File

@ -8,10 +8,18 @@ from django.test import TransactionTestCase
from django.test.utils import override_settings 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): class ModWsgiHandlerTestCase(TransactionTestCase):
""" """
Tests for the mod_wsgi authentication handler Tests for the mod_wsgi authentication handler
""" """
available_apps = [
'django.contrib.auth',
'django.contrib.contenttypes',
]
@skipIfCustomUser @skipIfCustomUser
def test_check_password(self): def test_check_password(self):
""" """

View File

@ -2,9 +2,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
from django.contrib.auth.hashers import (is_password_usable, from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
check_password, make_password, PBKDF2PasswordHasher, load_hashers, check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher,
PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD) get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
from django.utils import six
from django.utils import unittest from django.utils import unittest
from django.utils.unittest import skipUnless from django.utils.unittest import skipUnless
@ -31,6 +32,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(encoded)) self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', 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): def test_pkbdf2(self):
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256') encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@ -40,6 +47,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded)) self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256") 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): def test_sha1(self):
encoded = make_password('lètmein', 'seasalt', 'sha1') encoded = make_password('lètmein', 'seasalt', 'sha1')
@ -49,6 +62,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded)) self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "sha1") 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): def test_md5(self):
encoded = make_password('lètmein', 'seasalt', 'md5') encoded = make_password('lètmein', 'seasalt', 'md5')
@ -58,6 +77,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded)) self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "md5") 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): def test_unsalted_md5(self):
encoded = make_password('lètmein', '', 'unsalted_md5') encoded = make_password('lètmein', '', 'unsalted_md5')
@ -71,6 +96,11 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(alt_encoded)) self.assertTrue(is_password_usable(alt_encoded))
self.assertTrue(check_password('lètmein', alt_encoded)) self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', 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): def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1') encoded = make_password('lètmein', '', 'unsalted_sha1')
@ -82,6 +112,12 @@ class TestUtilsHashPass(unittest.TestCase):
# Raw SHA1 isn't acceptable # Raw SHA1 isn't acceptable
alt_encoded = encoded[6:] alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', alt_encoded)) 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.") @skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self): def test_crypt(self):
@ -91,6 +127,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmei', encoded)) self.assertTrue(check_password('lètmei', encoded))
self.assertFalse(check_password('lètmeiz', encoded)) self.assertFalse(check_password('lètmeiz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "crypt") 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") @skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt_sha256(self): def test_bcrypt_sha256(self):
@ -107,6 +149,12 @@ class TestUtilsHashPass(unittest.TestCase):
encoded = make_password(password, hasher='bcrypt_sha256') encoded = make_password(password, hasher='bcrypt_sha256')
self.assertTrue(check_password(password, encoded)) self.assertTrue(check_password(password, encoded))
self.assertFalse(check_password(password[:72], 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") @skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt(self): def test_bcrypt(self):
@ -116,21 +164,31 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded)) self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded)) self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt") 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): def test_unusable(self):
encoded = make_password(None) encoded = make_password(None)
self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
self.assertFalse(is_password_usable(encoded)) self.assertFalse(is_password_usable(encoded))
self.assertFalse(check_password(None, 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('', encoded))
self.assertFalse(check_password('lètmein', encoded)) self.assertFalse(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded)) self.assertFalse(check_password('lètmeinz', encoded))
self.assertRaises(ValueError, identify_hasher, 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 test_bad_algorithm(self):
def doit(): with self.assertRaises(ValueError):
make_password('lètmein', hasher='lolcat') make_password('lètmein', hasher='lolcat')
self.assertRaises(ValueError, doit)
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash") self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
def test_bad_encoded(self): def test_bad_encoded(self):
@ -178,3 +236,17 @@ class TestUtilsHashPass(unittest.TestCase):
state['upgraded'] = True state['upgraded'] = True
self.assertFalse(check_password('WRONG', encoded, setter)) self.assertFalse(check_password('WRONG', encoded, setter))
self.assertFalse(state['upgraded']) 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()

View File

@ -174,6 +174,13 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
class CustomUserModelValidationTestCase(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') @override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields')
def test_username_not_in_required_fields(self): def test_username_not_in_required_fields(self):
"USERNAME_FIELD should not appear in REQUIRED_FIELDS." "USERNAME_FIELD should not appear in REQUIRED_FIELDS."

View File

@ -87,7 +87,7 @@ class UserManagerTestCase(TestCase):
user = User.objects.create_user('user', email_lowercase) user = User.objects.create_user('user', email_lowercase)
self.assertEqual(user.email, email_lowercase) self.assertEqual(user.email, email_lowercase)
self.assertEqual(user.username, 'user') self.assertEqual(user.username, 'user')
self.assertEqual(user.password, '!') self.assertFalse(user.has_usable_password())
def test_create_user_email_domain_normalize_rfc3696(self): def test_create_user_email_domain_normalize_rfc3696(self):
# According to http://tools.ietf.org/html/rfc3696#section-3 # According to http://tools.ietf.org/html/rfc3696#section-3

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router 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.encoding import smart_text
from django.utils import six from django.utils import six
from django.utils.six.moves import input 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 Creates content types for models in the given app, removing any model
entries that no longer have a matching model class. entries that no longer have a matching model class.
""" """
try:
get_model('contenttypes', 'ContentType')
except UnavailableApp:
return
if not router.allow_syncdb(db, ContentType): if not router.allow_syncdb(db, ContentType):
return return

View File

@ -17,11 +17,17 @@ class FlatpageForm(forms.ModelForm):
def clean_url(self): def clean_url(self):
url = self.cleaned_data['url'] url = self.cleaned_data['url']
if not url.startswith('/'): 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 if (settings.APPEND_SLASH and
'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and 'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and
not url.endswith('/')): 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 return url
def clean(self): def clean(self):
@ -36,7 +42,9 @@ class FlatpageForm(forms.ModelForm):
for site in sites: for site in sites:
if same_url.filter(sites=site).exists(): if same_url.filter(sites=site).exists():
raise forms.ValidationError( raise forms.ValidationError(
_('Flatpage with url %(url)s already exists for site %(site)s') % _('Flatpage with url %(url)s already exists for site %(site)s'),
{'url': url, 'site': site}) code='duplicate_url',
params={'url': url, 'site': site},
)
return super(FlatpageForm, self).clean() return super(FlatpageForm, self).clean()

View File

@ -7,6 +7,7 @@ from django.forms import formsets, ValidationError
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext as _
from django.utils import six from django.utils import six
from django.contrib.formtools.wizard.storage import get_storage 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) management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid(): if not management_form.is_valid():
raise ValidationError( 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'] form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and if (form_current_step != self.steps.current and

View File

@ -50,7 +50,7 @@ class GeometryField(forms.Field):
try: try:
return GEOSGeometry(value) return GEOSGeometry(value)
except (GEOSException, ValueError, TypeError): 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): def clean(self, value):
""" """
@ -65,7 +65,7 @@ class GeometryField(forms.Field):
# Ensuring that the geometry is of the correct type (indicated # Ensuring that the geometry is of the correct type (indicated
# using the OGC string label). # using the OGC string label).
if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY': 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. # Transforming the geometry if the SRID was set.
if self.srid: if self.srid:
@ -76,7 +76,7 @@ class GeometryField(forms.Field):
try: try:
geom.transform(self.srid) geom.transform(self.srid)
except: except:
raise forms.ValidationError(self.error_messages['transform_error']) raise forms.ValidationError(self.error_messages['transform_error'], code='transform_error')
return geom return geom

View File

@ -22,7 +22,9 @@ def _simple_domain_name_validator(value):
checks = ((s in value) for s in string.whitespace) checks = ((s in value) for s in string.whitespace)
if any(checks): if any(checks):
raise ValidationError( 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): class SiteManager(models.Manager):

View File

@ -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

View File

@ -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]

View File

@ -3,6 +3,9 @@ Global Django exception and warning classes.
""" """
import logging import logging
from functools import reduce from functools import reduce
import operator
from django.utils.encoding import force_text
class DjangoRuntimeWarning(RuntimeWarning): class DjangoRuntimeWarning(RuntimeWarning):
@ -74,46 +77,65 @@ NON_FIELD_ERRORS = '__all__'
class ValidationError(Exception): class ValidationError(Exception):
"""An error while validating data.""" """An error while validating data."""
def __init__(self, message, code=None, params=None): 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 ValidationError can be passed any object that can be printed (usually
a string), a list of objects or a dictionary. a string), a list of objects or a dictionary.
""" """
if isinstance(message, dict): if isinstance(message, dict):
self.message_dict = message self.error_dict = message
# Reduce each list of messages into a single list. elif isinstance(message, list):
message = reduce(operator.add, message.values()) self.error_list = message
if isinstance(message, list):
self.messages = [force_text(msg) for msg in message]
else: else:
self.code = code self.code = code
self.params = params 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) message = force_text(message)
self.messages = [message] messages.append(message)
return messages
def __str__(self): def __str__(self):
# This is needed because, without a __str__(), printing an exception if hasattr(self, 'error_dict'):
# 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'):
return repr(self.message_dict) return repr(self.message_dict)
return repr(self.messages) return repr(self.messages)
def __repr__(self): def __repr__(self):
if hasattr(self, 'message_dict'): return 'ValidationError(%s)' % self
return 'ValidationError(%s)' % repr(self.message_dict)
return 'ValidationError(%s)' % repr(self.messages)
def update_error_dict(self, error_dict): def update_error_dict(self, error_dict):
if hasattr(self, 'message_dict'): if hasattr(self, 'error_dict'):
if 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) error_dict.setdefault(k, []).extend(v)
else: else:
error_dict = self.message_dict error_dict = self.error_dict
else: else:
error_dict[NON_FIELD_ERRORS] = self.messages error_dict[NON_FIELD_ERRORS] = self.error_list
return error_dict return error_dict

View File

@ -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)

View File

@ -32,8 +32,10 @@ class Command(NoArgsCommand):
connection = connections[db] connection = connections[db]
verbosity = int(options.get('verbosity')) verbosity = int(options.get('verbosity'))
interactive = options.get('interactive') 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) 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() self.style = no_style()
@ -45,7 +47,9 @@ class Command(NoArgsCommand):
except ImportError: except ImportError:
pass 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: if interactive:
confirm = input("""You have requested a flush of the database. 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" "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) "The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2]) 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 if not inhibit_post_syncdb:
# sync'd from scratch. self.emit_post_syncdb(verbosity, interactive, db)
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)
# Reinstall the initial_data fixture. # Reinstall the initial_data fixture.
if options.get('load_initial_data'): if options.get('load_initial_data'):
@ -90,3 +87,15 @@ Are you sure you want to do this?
else: else:
self.stdout.write("Flush cancelled.\n") 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)

View File

@ -40,6 +40,11 @@ class Command(BaseCommand):
return get_internal_wsgi_application() return get_internal_wsgi_application()
def handle(self, addrport='', *args, **options): 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') self.use_ipv6 = options.get('use_ipv6')
if self.use_ipv6 and not socket.has_ipv6: if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.') raise CommandError('Your Python does not support IPv6.')

View File

@ -102,7 +102,7 @@ def sql_delete(app, style, connection):
return output[::-1] # Reverse it, to deal with table dependencies. 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. 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: else:
tables = connection.introspection.table_names() tables = connection.introspection.table_names()
seqs = connection.introspection.sequence_list() if reset_sequences else () 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 return statements

View File

@ -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 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): 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. # Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
if cls.USERNAME_FIELD in cls.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.') e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')

View File

@ -422,8 +422,11 @@ class RegexURLResolver(LocaleRegexProvider):
lookup_view_s = "%s.%s" % (m, n) lookup_view_s = "%s.%s" % (m, n)
else: else:
lookup_view_s = lookup_view lookup_view_s = lookup_view
patterns = [pattern for (possibility, pattern, defaults) in possibilities]
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword " 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): class LocaleRegexURLResolver(RegexURLResolver):
""" """

View File

@ -76,7 +76,7 @@ def validate_integer(value):
try: try:
int(value) int(value)
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError('') raise ValidationError(_('Enter a valid integer.'), code='invalid')
class EmailValidator(object): class EmailValidator(object):
@ -188,11 +188,7 @@ class BaseValidator(object):
cleaned = self.clean(value) cleaned = self.clean(value)
params = {'limit_value': self.limit_value, 'show_value': cleaned} params = {'limit_value': self.limit_value, 'show_value': cleaned}
if self.compare(cleaned, self.limit_value): if self.compare(cleaned, self.limit_value):
raise ValidationError( raise ValidationError(self.message, code=self.code, params=params)
self.message % params,
code=self.code,
params=params,
)
class MaxValueValidator(BaseValidator): class MaxValueValidator(BaseValidator):

View File

@ -390,7 +390,7 @@ class BaseDatabaseWrapper(object):
def disable_constraint_checking(self): def disable_constraint_checking(self):
""" """
Backends can implement as needed to temporarily disable foreign key 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. disabled and will need to be reenabled.
""" """
return False return False
@ -966,7 +966,7 @@ class BaseDatabaseOperations(object):
""" """
return '' 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 Returns a list of SQL statements required to remove all data from
the given database tables (without actually removing the tables 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 The `style` argument is a Style object as returned by either
color_style() or no_style() in django.core.management.color. 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() raise NotImplementedError()

View File

@ -302,14 +302,17 @@ class DatabaseOperations(BaseDatabaseOperations):
def random_function_sql(self): def random_function_sql(self):
return 'RAND()' 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 # NB: The generated SQL below is specific to MySQL
# 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
# to clear all tables of all data # to clear all tables of all data
if tables: if tables:
sql = ['SET FOREIGN_KEY_CHECKS = 0;'] sql = ['SET FOREIGN_KEY_CHECKS = 0;']
for table in tables: 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.append('SET FOREIGN_KEY_CHECKS = 1;')
sql.extend(self.sequence_reset_by_name_sql(style, sequences)) sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql return sql

View File

@ -340,17 +340,17 @@ WHEN (new.%(col_name)s IS NULL)
def savepoint_rollback_sql(self, sid): def savepoint_rollback_sql(self, sid):
return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(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;', # Return a list of 'TRUNCATE x;', 'TRUNCATE y;',
# 'TRUNCATE z;'... style SQL statements # 'TRUNCATE z;'... style SQL statements
if tables: if tables:
# Oracle does support TRUNCATE, but it seems to get us into # Oracle does support TRUNCATE, but it seems to get us into
# FK referential trouble, whereas DELETE FROM table works. # FK referential trouble, whereas DELETE FROM table works.
sql = ['%s %s %s;' % \ sql = ['%s %s %s;' % (
(style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('DELETE'),
style.SQL_KEYWORD('FROM'), style.SQL_KEYWORD('FROM'),
style.SQL_FIELD(self.quote_name(table))) style.SQL_FIELD(self.quote_name(table))
for table in tables] ) for table in tables]
# Since we've just deleted all the rows, running our sequence # Since we've just deleted all the rows, running our sequence
# ALTER code will reset the sequence to 0. # ALTER code will reset the sequence to 0.
sql.extend(self.sequence_reset_by_name_sql(style, sequences)) sql.extend(self.sequence_reset_by_name_sql(style, sequences))

View File

@ -101,15 +101,24 @@ class DatabaseOperations(BaseDatabaseOperations):
def set_time_zone_sql(self): def set_time_zone_sql(self):
return "SET TIME ZONE %s" return "SET TIME ZONE %s"
def sql_flush(self, style, tables, sequences): def sql_flush(self, style, tables, sequences, allow_cascade=False):
if tables: if tables:
# Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows # Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows
# us to truncate tables referenced by a foreign key in any other # us to truncate tables referenced by a foreign key in any other
# table. # table.
sql = ['%s %s;' % \ tables_sql = ', '.join(
(style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table)) for table in tables)
style.SQL_FIELD(', '.join([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)) sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql return sql
else: else:

View File

@ -212,15 +212,15 @@ class DatabaseOperations(BaseDatabaseOperations):
def no_limit_value(self): def no_limit_value(self):
return -1 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 # NB: The generated SQL below is specific to SQLite
# Note: The DELETE FROM... SQL generated below works for SQLite databases # Note: The DELETE FROM... SQL generated below works for SQLite databases
# because constraints don't exist # because constraints don't exist
sql = ['%s %s %s;' % \ sql = ['%s %s %s;' % (
(style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('DELETE'),
style.SQL_KEYWORD('FROM'), style.SQL_KEYWORD('FROM'),
style.SQL_FIELD(self.quote_name(table)) style.SQL_FIELD(self.quote_name(table))
) for table in tables] ) for table in tables]
# Note: No requirement for reset of auto-incremented indices (cf. other # Note: No requirement for reset of auto-incremented indices (cf. other
# sql_flush() implementations). Just return SQL at this point # sql_flush() implementations). Just return SQL at this point
return sql return sql

View File

@ -1,7 +1,7 @@
from functools import wraps from functools import wraps
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured 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.query import Q
from django.db.models.expressions import F from django.db.models.expressions import F
from django.db.models.manager import Manager from django.db.models.manager import Manager

View File

@ -450,16 +450,18 @@ class Model(six.with_metaclass(ModelBase)):
need to do things manually, as they're dynamically created classes and need to do things manually, as they're dynamically created classes and
only module-level classes can be pickled by the default path. only module-level classes can be pickled by the default path.
""" """
if not self._deferred:
return super(Model, self).__reduce__()
data = self.__dict__ 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 = [] defers = []
for field in self._meta.fields: for field in self._meta.fields:
if isinstance(self.__class__.__dict__.get(field.attname), if isinstance(self.__class__.__dict__.get(field.attname),
DeferredAttribute): DeferredAttribute):
defers.append(field.attname) defers.append(field.attname)
model = self._meta.proxy_for_model 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): def _get_pk_val(self, meta=None):
if not meta: if not meta:
@ -907,7 +909,7 @@ class Model(six.with_metaclass(ModelBase)):
'field_label': six.text_type(field_labels) '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, Calls clean_fields, clean, and validate_unique, on the model,
and raises a ``ValidationError`` for any errors that occurred. 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) errors = e.update_error_dict(errors)
# Run unique checks, but only for fields that passed validation. # Run unique checks, but only for fields that passed validation.
for name in errors.keys(): if validate_unique:
if name != NON_FIELD_ERRORS and name not in exclude: for name in errors.keys():
exclude.append(name) if name != NON_FIELD_ERRORS and name not in exclude:
try: exclude.append(name)
self.validate_unique(exclude=exclude) try:
except ValidationError as e: self.validate_unique(exclude=exclude)
errors = e.update_error_dict(errors) except ValidationError as e:
errors = e.update_error_dict(errors)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
@ -960,7 +963,7 @@ class Model(six.with_metaclass(ModelBase)):
try: try:
setattr(self, f.attname, f.clean(raw_value, self)) setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e: except ValidationError as e:
errors[f.name] = e.messages errors[f.name] = e.error_list
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
@ -1007,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
class Empty(object): class Empty(object):
pass 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. 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) return cls.__new__(cls)
model_unpickle.__safe_for_unpickle__ = True model_unpickle.__safe_for_unpickle__ = True

View File

@ -77,7 +77,7 @@ class Field(object):
auto_creation_counter = -1 auto_creation_counter = -1
default_validators = [] # Default set of validators default_validators = [] # Default set of validators
default_error_messages = { 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.'), 'null': _('This field cannot be null.'),
'blank': _('This field cannot be blank.'), 'blank': _('This field cannot be blank.'),
'unique': _('%(model_name)s with this %(field_label)s ' 'unique': _('%(model_name)s with this %(field_label)s '
@ -294,12 +294,9 @@ class Field(object):
v(value) v(value)
except exceptions.ValidationError as e: except exceptions.ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages: if hasattr(e, 'code') and e.code in self.error_messages:
message = self.error_messages[e.code] e.message = self.error_messages[e.code]
if e.params: errors.extend(e.error_list)
message = message % e.params
errors.append(message)
else:
errors.extend(e.messages)
if errors: if errors:
raise exceptions.ValidationError(errors) raise exceptions.ValidationError(errors)
@ -322,14 +319,17 @@ class Field(object):
return return
elif value == option_key: elif value == option_key:
return return
msg = self.error_messages['invalid_choice'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid_choice'],
code='invalid_choice',
params={'value': value},
)
if value is None and not self.null: 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: 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): def clean(self, value, model_instance):
""" """
@ -678,7 +678,7 @@ class AutoField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be an integer."), 'invalid': _("'%(value)s' value must be an integer."),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -702,8 +702,11 @@ class AutoField(Field):
try: try:
return int(value) return int(value)
except (TypeError, ValueError): except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def validate(self, value, model_instance): def validate(self, value, model_instance):
pass pass
@ -732,7 +735,7 @@ class AutoField(Field):
class BooleanField(Field): class BooleanField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { 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)") description = _("Boolean (Either True or False)")
@ -757,8 +760,11 @@ class BooleanField(Field):
return True return True
if value in ('f', 'False', '0'): if value in ('f', 'False', '0'):
return False return False
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the # Special-case handling for filters coming from a Web request (e.g. the
@ -830,9 +836,9 @@ class CommaSeparatedIntegerField(CharField):
class DateField(Field): class DateField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { 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."), "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."), "but it is an invalid date."),
} }
description = _("Date (without time)") description = _("Date (without time)")
@ -878,11 +884,17 @@ class DateField(Field):
if parsed is not None: if parsed is not None:
return parsed return parsed
except ValueError: except ValueError:
msg = self.error_messages['invalid_date'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid_date'],
code='invalid_date',
params={'value': value},
)
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add): if self.auto_now or (self.auto_now_add and add):
@ -930,11 +942,11 @@ class DateField(Field):
class DateTimeField(DateField): class DateTimeField(DateField):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { 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."), "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."), "(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]) " "(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) "
"but it is an invalid date/time."), "but it is an invalid date/time."),
} }
@ -969,19 +981,28 @@ class DateTimeField(DateField):
if parsed is not None: if parsed is not None:
return parsed return parsed
except ValueError: except ValueError:
msg = self.error_messages['invalid_datetime'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid_datetime'],
code='invalid_datetime',
params={'value': value},
)
try: try:
parsed = parse_date(value) parsed = parse_date(value)
if parsed is not None: if parsed is not None:
return datetime.datetime(parsed.year, parsed.month, parsed.day) return datetime.datetime(parsed.year, parsed.month, parsed.day)
except ValueError: except ValueError:
msg = self.error_messages['invalid_date'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid_date'],
code='invalid_date',
params={'value': value},
)
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and add): if self.auto_now or (self.auto_now_add and add):
@ -1027,7 +1048,7 @@ class DateTimeField(DateField):
class DecimalField(Field): class DecimalField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be a decimal number."), 'invalid': _("'%(value)s' value must be a decimal number."),
} }
description = _("Decimal number") description = _("Decimal number")
@ -1053,8 +1074,11 @@ class DecimalField(Field):
try: try:
return decimal.Decimal(value) return decimal.Decimal(value)
except decimal.InvalidOperation: except decimal.InvalidOperation:
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def _format(self, value): def _format(self, value):
if isinstance(value, six.string_types) or value is None: if isinstance(value, six.string_types) or value is None:
@ -1162,7 +1186,7 @@ class FilePathField(Field):
class FloatField(Field): class FloatField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be a float."), 'invalid': _("'%(value)s' value must be a float."),
} }
description = _("Floating point number") description = _("Floating point number")
@ -1180,8 +1204,11 @@ class FloatField(Field):
try: try:
return float(value) return float(value)
except (TypeError, ValueError): except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.FloatField} defaults = {'form_class': forms.FloatField}
@ -1191,7 +1218,7 @@ class FloatField(Field):
class IntegerField(Field): class IntegerField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { default_error_messages = {
'invalid': _("'%s' value must be an integer."), 'invalid': _("'%(value)s' value must be an integer."),
} }
description = _("Integer") description = _("Integer")
@ -1215,8 +1242,11 @@ class IntegerField(Field):
try: try:
return int(value) return int(value)
except (TypeError, ValueError): except (TypeError, ValueError):
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {'form_class': forms.IntegerField} defaults = {'form_class': forms.IntegerField}
@ -1314,7 +1344,7 @@ class GenericIPAddressField(Field):
class NullBooleanField(Field): class NullBooleanField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { 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)") description = _("Boolean (Either True, False or None)")
@ -1343,8 +1373,11 @@ class NullBooleanField(Field):
return True return True
if value in ('f', 'False', '0'): if value in ('f', 'False', '0'):
return False return False
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
# Special-case handling for filters coming from a Web request (e.g. the # 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) return super(PositiveSmallIntegerField, self).formfield(**defaults)
class SlugField(CharField): class SlugField(CharField):
default_validators = [validators.validate_slug]
description = _("Slug (up to %(max_length)s)") description = _("Slug (up to %(max_length)s)")
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -1445,9 +1479,9 @@ class TextField(Field):
class TimeField(Field): class TimeField(Field):
empty_strings_allowed = False empty_strings_allowed = False
default_error_messages = { 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."), "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."), "(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."),
} }
description = _("Time") description = _("Time")
@ -1487,11 +1521,17 @@ class TimeField(Field):
if parsed is not None: if parsed is not None:
return parsed return parsed
except ValueError: except ValueError:
msg = self.error_messages['invalid_time'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid_time'],
code='invalid_time',
params={'value': value},
)
msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(
raise exceptions.ValidationError(msg) self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
def pre_save(self, model_instance, add): def pre_save(self, model_instance, add):
if self.auto_now or (self.auto_now_add and 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) return super(TimeField, self).formfield(**defaults)
class URLField(CharField): class URLField(CharField):
default_validators = [validators.URLValidator()]
description = _("URL") description = _("URL")
def __init__(self, verbose_name=None, name=None, **kwargs): def __init__(self, verbose_name=None, name=None, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200) kwargs['max_length'] = kwargs.get('max_length', 200)
CharField.__init__(self, verbose_name, name, **kwargs) CharField.__init__(self, verbose_name, name, **kwargs)
self.validators.append(validators.URLValidator())
def deconstruct(self): def deconstruct(self):
name, path, args, kwargs = super(URLField, self).deconstruct() name, path, args, kwargs = super(URLField, self).deconstruct()

View File

@ -1194,8 +1194,11 @@ class ForeignKey(ForeignObject):
) )
qs = qs.complex_filter(self.rel.limit_choices_to) qs = qs.complex_filter(self.rel.limit_choices_to)
if not qs.exists(): if not qs.exists():
raise exceptions.ValidationError(self.error_messages['invalid'] % { raise exceptions.ValidationError(
'model': self.rel.to._meta.verbose_name, 'pk': value}) self.error_messages['invalid'],
code='invalid',
params={'model': self.rel.to._meta.verbose_name, 'pk': value},
)
def get_attname(self): def get_attname(self):
return '%s_id' % self.name return '%s_id' % self.name

View File

@ -15,6 +15,8 @@ import os
__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
'load_app', 'app_cache_ready') 'load_app', 'app_cache_ready')
class UnavailableApp(Exception):
pass
def _initialize(): def _initialize():
""" """
@ -40,11 +42,12 @@ def _initialize():
# -- Everything below here is only used when populating the cache -- # -- Everything below here is only used when populating the cache --
loads_installed = True, loads_installed = True,
loaded = False, loaded=False,
handled = {}, handled=set(),
postponed = [], postponed=[],
nesting_level = 0, nesting_level=0,
_get_models_cache = {}, _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 Loads the app with the provided fully qualified name, and returns the
model module. model module.
""" """
self.handled[app_name] = None self.handled.add(app_name)
self.nesting_level += 1 self.nesting_level += 1
app_module = import_module(app_name) app_module = import_module(app_name)
try: try:
@ -157,12 +160,17 @@ class BaseAppCache(object):
""" """
self._populate() 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 # 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 # added at the end). This avoids unstable ordering on the admin app
# list page, for example. # list page, for example.
apps = [(v, k) for k, v in self.app_store.items()] apps = sorted(apps, key=lambda elt: elt[1])
apps.sort()
return [elt[1] for elt in apps] return [elt[0] for elt in apps]
def get_app_paths(self): def get_app_paths(self):
""" """
@ -183,8 +191,12 @@ class BaseAppCache(object):
def get_app(self, app_label, emptyOK=False): def get_app(self, app_label, emptyOK=False):
""" """
Returns the module containing the models for the given app_label. If Returns the module containing the models for the given app_label.
the app has no models in it and 'emptyOK' is True, returns None.
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() self._populate()
imp.acquire_lock() imp.acquire_lock()
@ -192,12 +204,11 @@ class BaseAppCache(object):
for app_name in settings.INSTALLED_APPS: for app_name in settings.INSTALLED_APPS:
if app_label == app_name.split('.')[-1]: if app_label == app_name.split('.')[-1]:
mod = self.load_app(app_name, False) mod = self.load_app(app_name, False)
if mod is None: if mod is None and not emptyOK:
if emptyOK:
return None
raise ImproperlyConfigured("App with label %s is missing a models.py module." % app_label) raise ImproperlyConfigured("App with label %s is missing a models.py module." % app_label)
else: if self.available_apps is not None and app_label not in self.available_apps:
return mod 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) raise ImproperlyConfigured("App with label %s could not be found" % app_label)
finally: finally:
imp.release_lock() imp.release_lock()
@ -234,8 +245,13 @@ class BaseAppCache(object):
if not self.loads_installed: if not self.loads_installed:
only_installed = False only_installed = False
cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped) cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
model_list = None
try: 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: except KeyError:
pass pass
self._populate() self._populate()
@ -260,6 +276,9 @@ class BaseAppCache(object):
(not model._meta.swapped or include_swapped)) (not model._meta.swapped or include_swapped))
) )
self._get_models_cache[cache_key] = model_list 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 return model_list
def get_model(self, app_label, model_name, def get_model(self, app_label, model_name,
@ -269,6 +288,9 @@ class BaseAppCache(object):
model_name. model_name.
Returns None if no model is found. 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: if not self.loads_installed:
only_installed = False only_installed = False
@ -276,7 +298,13 @@ class BaseAppCache(object):
self._populate() self._populate()
if only_installed and app_label not in self.app_labels: if only_installed and app_label not in self.app_labels:
return None 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): def register_models(self, app_label, *models):
""" """
@ -301,6 +329,16 @@ class BaseAppCache(object):
model_dict[model_name] = model model_dict[model_name] = model
self._get_models_cache.clear() 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): class AppCache(BaseAppCache):
""" """
@ -315,6 +353,7 @@ class AppCache(BaseAppCache):
def __init__(self): def __init__(self):
self.__dict__ = self.__shared_state self.__dict__ = self.__shared_state
cache = AppCache() cache = AppCache()

View File

@ -22,6 +22,12 @@ class SQLCompiler(object):
self.connection = connection self.connection = connection
self.using = using self.using = using
self.quote_cache = {} 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): def pre_sql_setup(self):
""" """
@ -74,7 +80,7 @@ class SQLCompiler(object):
# another run of it. # another run of it.
self.refcounts_before = self.query.alias_refcount.copy() self.refcounts_before = self.query.alias_refcount.copy()
out_cols, s_params = self.get_columns(with_col_aliases) 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() distinct_fields = self.get_distinct()
@ -95,9 +101,10 @@ class SQLCompiler(object):
if self.query.distinct: if self.query.distinct:
result.append(self.connection.ops.distinct_sql(distinct_fields)) result.append(self.connection.ops.distinct_sql(distinct_fields))
params.extend(o_params)
result.append(', '.join(out_cols + self.query.ordering_aliases)) result.append(', '.join(out_cols + self.ordering_aliases))
params.extend(s_params) params.extend(s_params)
params.extend(self.ordering_params)
result.append('FROM') result.append('FROM')
result.extend(from_) result.extend(from_)
@ -319,7 +326,6 @@ class SQLCompiler(object):
result.append("%s.%s" % (qn(alias), qn2(col))) result.append("%s.%s" % (qn(alias), qn2(col)))
return result return result
def get_ordering(self): def get_ordering(self):
""" """
Returns a tuple containing a list representing the SQL elements in the 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. # the table/column pairs we use and discard any after the first use.
processed_pairs = set() processed_pairs = set()
for field in ordering: params = []
ordering_params = []
for pos, field in enumerate(ordering):
if field == '?': if field == '?':
result.append(self.connection.ops.random_function_sql()) result.append(self.connection.ops.random_function_sql())
continue continue
@ -384,7 +392,7 @@ class SQLCompiler(object):
if not distinct or elt in select_aliases: if not distinct or elt in select_aliases:
result.append('%s %s' % (elt, order)) result.append('%s %s' % (elt, order))
group_by.append((elt, [])) 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 # 'col' is of the form 'field' or 'field1__field2' or
# '-field1__field2__field', etc. # '-field1__field2__field', etc.
for table, cols, order in self.find_ordering_name(field, for table, cols, order in self.find_ordering_name(field,
@ -399,12 +407,19 @@ class SQLCompiler(object):
group_by.append((elt, [])) group_by.append((elt, []))
else: else:
elt = qn2(col) elt = qn2(col)
if distinct and col not in select_aliases: if col not in self.query.extra_select:
ordering_aliases.append(elt) 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)) result.append('%s %s' % (elt, order))
group_by.append(self.query.extra_select[col]) group_by.append(self.query.extra[col])
self.query.ordering_aliases = ordering_aliases self.ordering_aliases = ordering_aliases
return result, group_by self.ordering_params = ordering_params
return result, params, group_by
def find_ordering_name(self, name, opts, alias=None, default_order='ASC', def find_ordering_name(self, name, opts, alias=None, default_order='ASC',
already_seen=None): already_seen=None):
@ -631,12 +646,10 @@ class SQLCompiler(object):
if not select_related_descend(f, restricted, requested, if not select_related_descend(f, restricted, requested,
only_load.get(field_model)): only_load.get(field_model)):
continue continue
table = f.rel.to._meta.db_table
promote = nullable or f.null promote = nullable or f.null
alias = self.query.join_parent_model(opts, model, root_alias, {}) _, _, _, joins, _ = self.query.setup_joins(
join_cols = f.get_joining_columns() [f.name], opts, root_alias, outer_if_first=promote)
alias = self.query.join((alias, table, join_cols), alias = joins[-1]
outer_if_first=promote, join_field=f)
columns, aliases = self.get_default_columns(start_alias=alias, columns, aliases = self.get_default_columns(start_alias=alias,
opts=f.rel.to._meta, as_pairs=True) opts=f.rel.to._meta, as_pairs=True)
self.query.related_select_cols.extend( self.query.related_select_cols.extend(
@ -660,12 +673,9 @@ class SQLCompiler(object):
only_load.get(model), reverse=True): only_load.get(model), reverse=True):
continue continue
alias = self.query.join_parent_model(opts, f.rel.to, root_alias, {}) _, _, _, joins, _ = self.query.setup_joins(
table = model._meta.db_table [f.related_query_name()], opts, root_alias, outer_if_first=True)
alias = self.query.join( alias = joins[-1]
(alias, table, f.get_joining_columns(reverse_join=True)),
outer_if_first=True, join_field=f
)
from_parent = (opts.model if issubclass(model, opts.model) from_parent = (opts.model if issubclass(model, opts.model)
else None) else None)
columns, aliases = self.get_default_columns(start_alias=alias, 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 # Use True here because we are looking at the _reverse_ side of
# the relation, which is always nullable. # the relation, which is always nullable.
new_nullable = True new_nullable = True
table = model._meta.db_table
self.fill_related_selections(model._meta, table, cur_depth+1, self.fill_related_selections(model._meta, table, cur_depth+1,
next, restricted, new_nullable) next, restricted, new_nullable)
@ -769,13 +779,13 @@ class SQLCompiler(object):
if not result_type: if not result_type:
return cursor return cursor
if result_type == SINGLE: if result_type == SINGLE:
if self.query.ordering_aliases: if self.ordering_aliases:
return cursor.fetchone()[:-len(self.query.ordering_aliases)] return cursor.fetchone()[:-len(self.ordering_aliases)]
return cursor.fetchone() return cursor.fetchone()
# The MULTI case. # The MULTI case.
if self.query.ordering_aliases: if self.ordering_aliases:
result = order_modified_iter(cursor, len(self.query.ordering_aliases), result = order_modified_iter(cursor, len(self.ordering_aliases),
self.connection.features.empty_fetchmany_value) self.connection.features.empty_fetchmany_value)
else: else:
result = iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)), result = iter((lambda: cursor.fetchmany(GET_ITERATOR_CHUNK_SIZE)),

View File

@ -115,7 +115,6 @@ class Query(object):
self.default_cols = True self.default_cols = True
self.default_ordering = True self.default_ordering = True
self.standard_ordering = True self.standard_ordering = True
self.ordering_aliases = []
self.used_aliases = set() self.used_aliases = set()
self.filter_is_sticky = False self.filter_is_sticky = False
self.included_inherited_models = {} self.included_inherited_models = {}
@ -227,7 +226,6 @@ class Query(object):
obj.default_ordering = self.default_ordering obj.default_ordering = self.default_ordering
obj.standard_ordering = self.standard_ordering obj.standard_ordering = self.standard_ordering
obj.included_inherited_models = self.included_inherited_models.copy() obj.included_inherited_models = self.included_inherited_models.copy()
obj.ordering_aliases = []
obj.select = self.select[:] obj.select = self.select[:]
obj.related_select_cols = [] obj.related_select_cols = []
obj.tables = self.tables[:] obj.tables = self.tables[:]
@ -926,10 +924,10 @@ class Query(object):
""" """
if model in seen: if model in seen:
return seen[model] return seen[model]
int_opts = opts
chain = opts.get_base_chain(model) chain = opts.get_base_chain(model)
if chain is None: if chain is None:
return alias return alias
curr_opts = opts
for int_model in chain: for int_model in chain:
if int_model in seen: if int_model in seen:
return seen[int_model] return seen[int_model]
@ -937,14 +935,14 @@ class Query(object):
# with no parents, assign the new options # with no parents, assign the new options
# object and skip to the next base in that # object and skip to the next base in that
# case # case
if not int_opts.parents[int_model]: if not curr_opts.parents[int_model]:
int_opts = int_model._meta curr_opts = int_model._meta
continue continue
link_field = int_opts.get_ancestor_link(int_model) link_field = curr_opts.get_ancestor_link(int_model)
int_opts = int_model._meta _, _, _, joins, _ = self.setup_joins(
connection = (alias, int_opts.db_table, link_field.get_joining_columns()) [link_field.name], curr_opts, alias)
alias = seen[int_model] = self.join(connection, nullable=False, curr_opts = int_model._meta
join_field=link_field) alias = seen[int_model] = joins[-1]
return alias or seen[None] return alias or seen[None]
def remove_inherited_models(self): def remove_inherited_models(self):
@ -1321,7 +1319,7 @@ class Query(object):
return path, final_field, targets return path, final_field, targets
def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True, 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 Compute the necessary table joins for the passage through the fields
given in 'names'. 'opts' is the Options class for the current model given in 'names'. 'opts' is the Options class for the current model
@ -1364,8 +1362,9 @@ class Query(object):
nullable = True nullable = True
connection = alias, opts.db_table, join.join_field.get_joining_columns() connection = alias, opts.db_table, join.join_field.get_joining_columns()
reuse = can_reuse if join.m2m else None reuse = can_reuse if join.m2m else None
alias = self.join(connection, reuse=reuse, alias = self.join(
nullable=nullable, join_field=join.join_field) connection, reuse=reuse, nullable=nullable, join_field=join.join_field,
outer_if_first=outer_if_first)
joins.append(alias) joins.append(alias)
if hasattr(final_field, 'field'): if hasattr(final_field, 'field'):
final_field = 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 Given the before and after copies of refcounts works out which aliases
have been added to the after copy. 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 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))

View File

@ -125,7 +125,7 @@ class Field(object):
def validate(self, value): def validate(self, value):
if value in self.empty_values and self.required: 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): def run_validators(self, value):
if value in self.empty_values: if value in self.empty_values:
@ -136,12 +136,8 @@ class Field(object):
v(value) v(value)
except ValidationError as e: except ValidationError as e:
if hasattr(e, 'code') and e.code in self.error_messages: if hasattr(e, 'code') and e.code in self.error_messages:
message = self.error_messages[e.code] e.message = self.error_messages[e.code]
if e.params: errors.extend(e.error_list)
message = message % e.params
errors.append(message)
else:
errors.extend(e.messages)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
@ -250,7 +246,7 @@ class IntegerField(Field):
try: try:
value = int(str(value)) value = int(str(value))
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
return value return value
def widget_attrs(self, widget): def widget_attrs(self, widget):
@ -281,7 +277,7 @@ class FloatField(IntegerField):
try: try:
value = float(value) value = float(value)
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
return value return value
def widget_attrs(self, widget): def widget_attrs(self, widget):
@ -327,7 +323,7 @@ class DecimalField(IntegerField):
try: try:
value = Decimal(value) value = Decimal(value)
except DecimalException: except DecimalException:
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
return value return value
def validate(self, 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 # 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 # isn't equal to itself, so we can use this to identify NaN
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"): 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() sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent) decimals = abs(exponent)
# digittuple doesn't include any leading zeros. # digittuple doesn't include any leading zeros.
@ -352,15 +348,24 @@ class DecimalField(IntegerField):
whole_digits = digits - decimals whole_digits = digits - decimals
if self.max_digits is not None and digits > self.max_digits: if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_digits'] % { raise ValidationError(
'max': self.max_digits}) 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: if self.decimal_places is not None and decimals > self.decimal_places:
raise ValidationError(self.error_messages['max_decimal_places'] % { raise ValidationError(
'max': self.decimal_places}) 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 if (self.max_digits is not None and self.decimal_places is not None
and whole_digits > (self.max_digits - self.decimal_places)): and whole_digits > (self.max_digits - self.decimal_places)):
raise ValidationError(self.error_messages['max_whole_digits'] % { raise ValidationError(
'max': (self.max_digits - self.decimal_places)}) self.error_messages['max_whole_digits'],
code='max_whole_digits',
params={'max': (self.max_digits - self.decimal_places)},
)
return value return value
def widget_attrs(self, widget): def widget_attrs(self, widget):
@ -395,7 +400,7 @@ class BaseTemporalField(Field):
return self.strptime(value, format) return self.strptime(value, format)
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
def strptime(self, value, format): def strptime(self, value, format):
raise NotImplementedError('Subclasses must define this method.') 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 # Input comes from a SplitDateTimeWidget, for example. So, it's two
# components: date and time. # components: date and time.
if len(value) != 2: 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: if value[0] in self.empty_values and value[1] in self.empty_values:
return None return None
value = '%s %s' % tuple(value) value = '%s %s' % tuple(value)
@ -552,22 +557,22 @@ class FileField(Field):
file_name = data.name file_name = data.name
file_size = data.size file_size = data.size
except AttributeError: 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: if self.max_length is not None and len(file_name) > self.max_length:
error_values = {'max': self.max_length, 'length': len(file_name)} params = {'max': self.max_length, 'length': len(file_name)}
raise ValidationError(self.error_messages['max_length'] % error_values) raise ValidationError(self.error_messages['max_length'], code='max_length', params=params)
if not file_name: 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: 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 return data
def clean(self, data, initial=None): def clean(self, data, initial=None):
# If the widget got contradictory inputs, we raise a validation error # If the widget got contradictory inputs, we raise a validation error
if data is FILE_INPUT_CONTRADICTION: 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 # False means the field value should be cleared; further validation is
# not needed. # not needed.
if data is False: if data is False:
@ -627,7 +632,10 @@ class ImageField(FileField):
Image.open(file).verify() Image.open(file).verify()
except Exception: except Exception:
# Pillow (or PIL) doesn't recognize it as an image. # 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): if hasattr(f, 'seek') and callable(f.seek):
f.seek(0) f.seek(0)
return f return f
@ -638,10 +646,7 @@ class URLField(CharField):
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid URL.'), 'invalid': _('Enter a valid URL.'),
} }
default_validators = [validators.URLValidator()]
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())
def to_python(self, value): def to_python(self, value):
@ -655,7 +660,7 @@ class URLField(CharField):
except ValueError: except ValueError:
# urlparse.urlsplit can raise a ValueError with some # urlparse.urlsplit can raise a ValueError with some
# misformatted URLs. # misformatted URLs.
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
value = super(URLField, self).to_python(value) value = super(URLField, self).to_python(value)
if value: if value:
@ -699,7 +704,7 @@ class BooleanField(Field):
def validate(self, value): def validate(self, value):
if not value and self.required: 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): def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the # Sometimes data or initial could be None or '' which should be the
@ -783,7 +788,11 @@ class ChoiceField(Field):
""" """
super(ChoiceField, self).validate(value) super(ChoiceField, self).validate(value)
if value and not self.valid_value(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): def valid_value(self, value):
"Check to see if the provided value is a valid choice" "Check to see if the provided value is a valid choice"
@ -817,7 +826,11 @@ class TypedChoiceField(ChoiceField):
try: try:
value = self.coerce(value) value = self.coerce(value)
except (ValueError, TypeError, ValidationError): 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 return value
@ -833,7 +846,7 @@ class MultipleChoiceField(ChoiceField):
if not value: if not value:
return [] return []
elif not isinstance(value, (list, tuple)): 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] return [smart_text(val) for val in value]
def validate(self, value): def validate(self, value):
@ -841,11 +854,15 @@ class MultipleChoiceField(ChoiceField):
Validates that the input is a list or tuple. Validates that the input is a list or tuple.
""" """
if self.required and not value: 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. # Validate that each value in the value list is in self.choices.
for val in value: for val in value:
if not self.valid_value(val): 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): def _has_changed(self, initial, data):
if initial is None: if initial is None:
@ -878,14 +895,18 @@ class TypedMultipleChoiceField(MultipleChoiceField):
try: try:
new_value.append(self.coerce(choice)) new_value.append(self.coerce(choice))
except (ValueError, TypeError, ValidationError): 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 return new_value
def validate(self, value): def validate(self, value):
if value != self.empty_value: if value != self.empty_value:
super(TypedMultipleChoiceField, self).validate(value) super(TypedMultipleChoiceField, self).validate(value)
elif self.required: elif self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'], code='required')
class ComboField(Field): class ComboField(Field):
@ -959,25 +980,25 @@ class MultiValueField(Field):
if not value or isinstance(value, (list, tuple)): 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 not value or not [v for v in value if v not in self.empty_values]:
if self.required: if self.required:
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'], code='required')
else: else:
return self.compress([]) return self.compress([])
else: else:
raise ValidationError(self.error_messages['invalid']) raise ValidationError(self.error_messages['invalid'], code='invalid')
for i, field in enumerate(self.fields): for i, field in enumerate(self.fields):
try: try:
field_value = value[i] field_value = value[i]
except IndexError: except IndexError:
field_value = None field_value = None
if self.required and field_value in self.empty_values: 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: try:
clean_data.append(field.clean(field_value)) clean_data.append(field.clean(field_value))
except ValidationError as e: except ValidationError as e:
# Collect all validation errors in a single list, which we'll # Collect all validation errors in a single list, which we'll
# raise at the end of clean(), rather than raising a single # raise at the end of clean(), rather than raising a single
# exception for the first error we encounter. # exception for the first error we encounter.
errors.extend(e.messages) errors.extend(e.error_list)
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)
@ -1085,9 +1106,9 @@ class SplitDateTimeField(MultiValueField):
# Raise a validation error if time or date is empty # Raise a validation error if time or date is empty
# (possible if SplitDateTimeField has required=False). # (possible if SplitDateTimeField has required=False).
if data_list[0] in self.empty_values: 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: 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) result = datetime.datetime.combine(*data_list)
return from_current_timezone(result) return from_current_timezone(result)
return None return None

View File

@ -170,11 +170,6 @@ class BaseForm(object):
if bf.label: if bf.label:
label = conditional_escape(force_text(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 '' label = bf.label_tag(label) or ''
else: else:
label = '' label = ''
@ -522,6 +517,9 @@ class BoundField(object):
If attrs are given, they're used as HTML attributes on the <label> tag. If attrs are given, they're used as HTML attributes on the <label> tag.
""" """
contents = contents or self.label 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 widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id id_ = widget.attrs.get('id') or self.auto_id
if id_: if id_:

View File

@ -85,7 +85,10 @@ class BaseFormSet(object):
if self.is_bound: if self.is_bound:
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix) form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
if not form.is_valid(): 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: else:
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={ form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(), TOTAL_FORM_COUNT: self.total_form_count(),
@ -263,6 +266,13 @@ class BaseFormSet(object):
self.full_clean() self.full_clean()
return self._errors 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): def _should_delete_form(self, form):
""" """
Returns whether or not the form was marked for deletion. 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: self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max:
raise ValidationError(ungettext( raise ValidationError(ungettext(
"Please submit %d or fewer forms.", "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. # Give self.clean() a chance to do cross-form validation.
self.clean() self.clean()
except ValidationError as e: except ValidationError as e:

View File

@ -138,7 +138,9 @@ def model_to_dict(instance, fields=None, exclude=None):
data[f.name] = f.value_from_object(instance) data[f.name] = f.value_from_object(instance)
return data 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. 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 fields will be excluded from the returned fields, even if they are listed
in the ``fields`` argument. 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 ``formfield_callback`` is a callable that takes a model field and returns
a form field. 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] kwargs['widget'] = widgets[f.name]
if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields):
kwargs['localize'] = True 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: if formfield_callback is None:
formfield = f.formfield(**kwargs) formfield = f.formfield(**kwargs)
@ -197,6 +214,9 @@ class ModelFormOptions(object):
self.exclude = getattr(options, 'exclude', None) self.exclude = getattr(options, 'exclude', None)
self.widgets = getattr(options, 'widgets', None) self.widgets = getattr(options, 'widgets', None)
self.localized_fields = getattr(options, 'localized_fields', 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): class ModelFormMetaclass(type):
@ -248,7 +268,9 @@ class ModelFormMetaclass(type):
opts.fields = None opts.fields = None
fields = fields_for_model(opts.model, opts.fields, opts.exclude, 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 # make sure opts.fields doesn't specify an invalid field
none_model_fields = [k for k, v in six.iteritems(fields) if not v] 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, super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
error_class, label_suffix, empty_permitted) 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(): for k, v in message_dict.items():
if k != NON_FIELD_ERRORS: if k != NON_FIELD_ERRORS:
self._errors.setdefault(k, self.error_class()).extend(v) self._errors.setdefault(k, self.error_class()).extend(v)
@ -367,17 +399,11 @@ class BaseModelForm(BaseForm):
if isinstance(field, InlineForeignKeyField): if isinstance(field, InlineForeignKeyField):
exclude.append(f_name) exclude.append(f_name)
# Clean the model instance's fields.
try: try:
self.instance.clean_fields(exclude=exclude) self.instance.full_clean(exclude=exclude,
validate_unique=False)
except ValidationError as e: except ValidationError as e:
self._update_errors(e.message_dict) self._update_errors(e)
# Call the model instance's clean method.
try:
self.instance.clean()
except ValidationError as e:
self._update_errors({NON_FIELD_ERRORS: e.messages})
# Validate uniqueness if needed. # Validate uniqueness if needed.
if self._validate_unique: if self._validate_unique:
@ -392,7 +418,7 @@ class BaseModelForm(BaseForm):
try: try:
self.instance.validate_unique(exclude=exclude) self.instance.validate_unique(exclude=exclude)
except ValidationError as e: except ValidationError as e:
self._update_errors(e.message_dict) self._update_errors(e)
def save(self, commit=True): def save(self, commit=True):
""" """
@ -416,7 +442,8 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)):
pass pass
def modelform_factory(model, form=ModelForm, fields=None, exclude=None, 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. 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 ``formfield_callback`` is a callable that takes a model field and returns
a form field. 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 # Create the inner Meta class. FIXME: ideally, we should be able to
# construct a ModelForm without creating and passing in a temporary # 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 attrs['widgets'] = widgets
if localized_fields is not None: if localized_fields is not None:
attrs['localized_fields'] = localized_fields 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 # If parent form class already has an inner Meta, the Meta we're
# creating needs to inherit from the parent's inner meta. # 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, def modelformset_factory(model, form=ModelForm, formfield_callback=None,
formset=BaseModelFormSet, extra=1, can_delete=False, formset=BaseModelFormSet, extra=1, can_delete=False,
can_order=False, max_num=None, fields=None, exclude=None, 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. 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, form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
formfield_callback=formfield_callback, 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, FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
can_order=can_order, can_delete=can_delete, can_order=can_order, can_delete=can_delete,
validate_max=validate_max) validate_max=validate_max)
@ -898,7 +940,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
formset=BaseInlineFormSet, fk_name=None, formset=BaseInlineFormSet, fk_name=None,
fields=None, exclude=None, extra=3, can_order=False, fields=None, exclude=None, extra=3, can_order=False,
can_delete=True, max_num=None, formfield_callback=None, 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. Returns an ``InlineFormSet`` for the given kwargs.
@ -922,6 +965,9 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
'widgets': widgets, 'widgets': widgets,
'validate_max': validate_max, 'validate_max': validate_max,
'localized_fields': localized_fields, 'localized_fields': localized_fields,
'labels': labels,
'help_texts': help_texts,
'error_messages': error_messages,
} }
FormSet = modelformset_factory(model, **kwargs) FormSet = modelformset_factory(model, **kwargs)
FormSet.fk = fk FormSet.fk = fk
@ -964,7 +1010,7 @@ class InlineForeignKeyField(Field):
else: else:
orig = self.parent_instance.pk orig = self.parent_instance.pk
if force_text(value) != force_text(orig): 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 return self.parent_instance
def _has_changed(self, initial, data): def _has_changed(self, initial, data):
@ -1079,7 +1125,7 @@ class ModelChoiceField(ChoiceField):
key = self.to_field_name or 'pk' key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value}) value = self.queryset.get(**{key: value})
except (ValueError, self.queryset.model.DoesNotExist): 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 return value
def validate(self, value): def validate(self, value):
@ -1114,22 +1160,30 @@ class ModelMultipleChoiceField(ModelChoiceField):
def clean(self, value): def clean(self, value):
if self.required and not 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: elif not self.required and not value:
return self.queryset.none() return self.queryset.none()
if not isinstance(value, (list, tuple)): 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' key = self.to_field_name or 'pk'
for pk in value: for pk in value:
try: try:
self.queryset.filter(**{key: pk}) self.queryset.filter(**{key: pk})
except ValueError: 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}) qs = self.queryset.filter(**{'%s__in' % key: value})
pks = set([force_text(getattr(o, key)) for o in qs]) pks = set([force_text(getattr(o, key)) for o in qs])
for val in value: for val in value:
if force_text(val) not in pks: 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 # Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here # we run custom validators here
self.run_validators(value) self.run_validators(value)

View File

@ -80,12 +80,17 @@ def from_current_timezone(value):
try: try:
return timezone.make_aware(value, current_timezone) return timezone.make_aware(value, current_timezone)
except Exception: except Exception:
msg = _( message = _(
'%(datetime)s couldn\'t be interpreted ' '%(datetime)s couldn\'t be interpreted '
'in time zone %(current_timezone)s; it ' 'in time zone %(current_timezone)s; it '
'may be ambiguous or it may not exist.') % {'datetime': value, 'current_timezone': 'may be ambiguous or it may not exist.'
current_timezone} )
six.reraise(ValidationError, ValidationError(msg), sys.exc_info()[2]) params = {'datetime': value, 'current_timezone': current_timezone}
six.reraise(ValidationError, ValidationError(
message,
code='ambiguous_timezone',
params=params,
), sys.exc_info()[2])
return value return value
def to_current_timezone(value): def to_current_timezone(value):

View File

@ -66,6 +66,9 @@ REASON_PHRASES = {
423: 'LOCKED', 423: 'LOCKED',
424: 'FAILED DEPENDENCY', 424: 'FAILED DEPENDENCY',
426: 'UPGRADE REQUIRED', 426: 'UPGRADE REQUIRED',
428: 'PRECONDITION REQUIRED',
429: 'TOO MANY REQUESTS',
431: 'REQUEST HEADER FIELDS TOO LARGE',
500: 'INTERNAL SERVER ERROR', 500: 'INTERNAL SERVER ERROR',
501: 'NOT IMPLEMENTED', 501: 'NOT IMPLEMENTED',
502: 'BAD GATEWAY', 502: 'BAD GATEWAY',
@ -76,6 +79,7 @@ REASON_PHRASES = {
507: 'INSUFFICIENT STORAGE', 507: 'INSUFFICIENT STORAGE',
508: 'LOOP DETECTED', 508: 'LOOP DETECTED',
510: 'NOT EXTENDED', 510: 'NOT EXTENDED',
511: 'NETWORK AUTHENTICATION REQUIRED',
} }

View File

@ -272,7 +272,6 @@ class RequestFactory(object):
parsed = urlparse(path) parsed = urlparse(path)
r = { r = {
'CONTENT_TYPE': str('text/html; charset=utf-8'),
'PATH_INFO': self._get_path(parsed), 'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]), 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
'REQUEST_METHOD': str('GET'), 'REQUEST_METHOD': str('GET'),
@ -303,7 +302,6 @@ class RequestFactory(object):
parsed = urlparse(path) parsed = urlparse(path)
r = { r = {
'CONTENT_TYPE': str('text/html; charset=utf-8'),
'PATH_INFO': self._get_path(parsed), 'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]), 'QUERY_STRING': urlencode(data, doseq=True) or force_str(parsed[4]),
'REQUEST_METHOD': str('HEAD'), 'REQUEST_METHOD': str('HEAD'),

View File

@ -24,10 +24,12 @@ from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from django.core.management import call_command from django.core.management import call_command
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.commands import flush
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer, from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer,
WSGIServerException) WSGIServerException)
from django.core.urlresolvers import clear_url_caches, set_urlconf from django.core.urlresolvers import clear_url_caches, set_urlconf
from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction 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.forms.fields import CharField
from django.http import QueryDict from django.http import QueryDict
from django.test.client import Client from django.test.client import Client
@ -195,9 +197,9 @@ class SimpleTestCase(ut2.TestCase):
def _pre_setup(self): def _pre_setup(self):
"""Performs any pre-test setup. This includes: """Performs any pre-test setup. This includes:
* If the Test Case class has a 'urls' member, replace the * Creating a test client.
ROOT_URLCONF with it. * If the class has a 'urls' attribute, replace ROOT_URLCONF with it.
* Clearing the mail test outbox. * Clearing the mail test outbox.
""" """
self.client = self.client_class() self.client = self.client_class()
self._urlconf_setup() self._urlconf_setup()
@ -211,6 +213,10 @@ class SimpleTestCase(ut2.TestCase):
clear_url_caches() clear_url_caches()
def _post_teardown(self): def _post_teardown(self):
"""Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed.
"""
self._urlconf_teardown() self._urlconf_teardown()
def _urlconf_teardown(self): def _urlconf_teardown(self):
@ -725,15 +731,29 @@ class TransactionTestCase(SimpleTestCase):
# test case # test case
reset_sequences = False reset_sequences = False
# Subclasses can enable only a subset of apps for faster tests
available_apps = None
def _pre_setup(self): def _pre_setup(self):
"""Performs any pre-test setup. This includes: """Performs any pre-test setup. This includes:
* Flushing the database. * If the class has an 'available_apps' attribute, restricting the app
* If the Test Case class has a 'fixtures' member, installing the cache to these applications, then firing post_syncdb -- it must run
named fixtures. 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() 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): def _databases_names(self, include_mirrors=True):
# If the test case has a multi_db=True flag, act on all databases, # 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): def _post_teardown(self):
"""Performs any post-test things. This includes: """Performs any post-test things. This includes:
* Putting back the original ROOT_URLCONF if it was changed. * Flushing the contents of the database, to leave a clean slate. If
* Force closing the connection, so that the next test gets the class has an 'available_apps' attribute, post_syncdb isn't fired.
a clean cursor. * Force-closing the connection, so the next test gets a clean cursor.
""" """
self._fixture_teardown() try:
super(TransactionTestCase, self)._post_teardown() self._fixture_teardown()
# Some DB cursors include SQL statements as part of cursor super(TransactionTestCase, self)._post_teardown()
# creation. If you have a test that does rollback, the effect # Some DB cursors include SQL statements as part of cursor
# of these statements is lost, which can effect the operation # creation. If you have a test that does rollback, the effect of
# of tests (e.g., losing a timezone setting causing objects to # these statements is lost, which can effect the operation of
# be created with the wrong time). # tests (e.g., losing a timezone setting causing objects to be
# To make sure this doesn't happen, get a clean connection at the # created with the wrong time). To make sure this doesn't happen,
# start of every test. # get a clean connection at the start of every test.
for conn in connections.all(): for conn in connections.all():
conn.close() conn.close()
finally:
cache.unset_available_apps()
def _fixture_teardown(self): 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): for db_name in self._databases_names(include_mirrors=False):
call_command('flush', verbosity=0, interactive=False, database=db_name, call_command('flush', verbosity=0, interactive=False,
skip_validation=True, reset_sequences=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): def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True):
items = six.moves.map(transform, qs) items = six.moves.map(transform, qs)

View File

@ -2,10 +2,11 @@
# Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/ # Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/
# Licensed under the Apache License, Version 2.0 (the "License"). # Licensed under the Apache License, Version 2.0 (the "License").
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django.utils.six.moves import xrange from django.utils.six.moves import xrange
def clean_ipv6_address(ip_str, unpack_ipv4=False, 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. Cleans a IPv6 address string.
@ -31,7 +32,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False,
doublecolon_len = 0 doublecolon_len = 0
if not is_valid_ipv6_address(ip_str): 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 # This algorithm can only handle fully exploded
# IP strings # IP strings

View File

@ -5,6 +5,7 @@ from functools import update_wrapper
from django import http from django import http
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse, NoReverseMatch
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils import six from django.utils import six
@ -160,9 +161,10 @@ class RedirectView(View):
""" """
permanent = True permanent = True
url = None url = None
pattern_name = None
query_string = False 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 Return the URL redirect to. Keyword arguments from the
URL pattern match generating the redirect request URL pattern match generating the redirect request
@ -170,15 +172,21 @@ class RedirectView(View):
""" """
if self.url: if self.url:
url = self.url % kwargs url = self.url % kwargs
args = self.request.META.get('QUERY_STRING', '') elif self.pattern_name:
if args and self.query_string: try:
url = "%s?%s" % (url, args) url = reverse(self.pattern_name, args=args, kwargs=kwargs)
return url except NoReverseMatch:
return None
else: else:
return None 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): def get(self, request, *args, **kwargs):
url = self.get_redirect_url(**kwargs) url = self.get_redirect_url(*args, **kwargs)
if url: if url:
if self.permanent: if self.permanent:
return http.HttpResponsePermanentRedirect(url) return http.HttpResponsePermanentRedirect(url)

View File

@ -242,8 +242,8 @@ class DeletionMixin(object):
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
# Add support for browsers which only accept GET and POST for now. # Add support for browsers which only accept GET and POST for now.
def post(self, *args, **kwargs): def post(self, request, *args, **kwargs):
return self.delete(*args, **kwargs) return self.delete(request, *args, **kwargs)
def get_success_url(self): def get_success_url(self):
if self.success_url: if self.success_url:

View File

@ -6,11 +6,12 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build SPHINXBUILD = sphinx-build
PAPER = PAPER =
BUILDDIR = _build BUILDDIR = _build
LANGUAGE =
# Internal variables. # Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees -D language=$(LANGUAGE) $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others # the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

View File

@ -77,6 +77,9 @@ django_next_version = '1.6'
# for a list of supported languages. # for a list of supported languages.
#language = None #language = None
# Location for .po/.mo translation files used when language is set
locale_dirs = ['locale/']
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = '' #today = ''

View File

@ -82,6 +82,14 @@ to combine a Django application with a WSGI application of another framework.
.. _`WSGI middleware`: http://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides .. _`WSGI middleware`: http://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides
.. note::
Some third-party WSGI middleware do not call ``close`` on the response
object after handling a request — most notably Sentry's error reporting
middleware up to version 2.0.7. In those cases the
:data:`~django.core.signals.request_finished` signal isn't sent. This can
result in idle connections to database and memcache servers.
Upgrading from Django < 1.4 Upgrading from Django < 1.4
--------------------------- ---------------------------

View File

@ -34,6 +34,15 @@ command. For example:
.. _installation procedures: http://projects.unbit.it/uwsgi/wiki/Install .. _installation procedures: http://projects.unbit.it/uwsgi/wiki/Install
.. warning::
Some distributions, including Debian and Ubuntu, ship an outdated version
of uWSGI that does not conform to the WSGI specification. Versions prior to
1.2.6 do not call ``close`` on the response object after handling a
request. In those cases the :data:`~django.core.signals.request_finished`
signal isn't sent. This can result in idle connections to database and
memcache servers.
uWSGI model uWSGI model
----------- -----------

View File

@ -526,6 +526,17 @@ Marc Tamlyn
.. _CCBV: http://ccbv.co.uk/ .. _CCBV: http://ccbv.co.uk/
.. _Incuna Ltd: http://incuna.com/ .. _Incuna Ltd: http://incuna.com/
Baptiste Mispelon
Baptiste discovered Django around the 1.2 version and promptly switched away
from his homegrown PHP framework. He started getting more involved in the
project after attending DjangoCon EU 2012, mostly by triaging tickets and
submitting small patches.
Baptiste currently lives in Budapest, Hungary and works for `M2BPO`_,
a small French company providing services to architects.
.. _M2BPO: http://www.m2bpo.fr
Developers Emeritus Developers Emeritus
=================== ===================

View File

@ -37,7 +37,7 @@ and time availability), claim it by following these steps:
`ticket tracker`_. `ticket tracker`_.
* If a ticket for this issue already exists, make sure nobody else has * If a ticket for this issue already exists, make sure nobody else has
claimed it. To do this, look at the "Assigned to" section of the ticket. claimed it. To do this, look at the "Owned by" section of the ticket.
If it's assigned to "nobody," then it's available to be claimed. If it's assigned to "nobody," then it's available to be claimed.
Otherwise, somebody else is working on this ticket, and you either find Otherwise, somebody else is working on this ticket, and you either find
another bug/feature to work on, or contact the developer working on the another bug/feature to work on, or contact the developer working on the
@ -48,7 +48,7 @@ and time availability), claim it by following these steps:
* Claim the ticket: * Claim the ticket:
1. click the "accept" radio button under "Action" near the bottom of the 1. click the "assign to myself" radio button under "Action" near the bottom of the
page, page,
2. then click "Submit changes." 2. then click "Submit changes."

View File

@ -473,6 +473,13 @@ template for all 404 errors when :setting:`DEBUG` is set to ``False`` (in your
settings module). If you do create the template, add at least some dummy settings module). If you do create the template, add at least some dummy
content like "Page not found". content like "Page not found".
.. warning::
If :setting:`DEBUG` is set to ``False``, all responses will be
"Bad Request (400)" unless you specify the proper :setting:`ALLOWED_HOSTS`
as well (something like ``['localhost', '127.0.0.1']`` for
local development).
A couple more things to note about 404 views: A couple more things to note about 404 views:
* If :setting:`DEBUG` is set to ``True`` (in your settings module) then your * If :setting:`DEBUG` is set to ``True`` (in your settings module) then your

View File

@ -192,22 +192,24 @@ RedirectView
permanent = False permanent = False
query_string = True query_string = True
pattern_name = 'article-detail'
def get_redirect_url(self, pk): def get_redirect_url(self, *args, **kwargs):
article = get_object_or_404(Article, pk=pk) article = get_object_or_404(Article, pk=pk)
article.update_counter() article.update_counter()
return reverse('product_detail', args=(pk,)) return super(ArticleCounterRedirectView, self).get_redirect_url(*args, **kwargs)
**Example urls.py**:: **Example urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from article.views import ArticleCounterRedirectView from article.views import ArticleCounterRedirectView, ArticleDetail
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'), url(r'^counter/(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'),
url(r'^details/(?P<pk>\d+)/$', ArticleDetail.as_view(), name='article-detail'),
url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'), url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'),
) )
@ -218,6 +220,11 @@ RedirectView
The URL to redirect to, as a string. Or ``None`` to raise a 410 (Gone) The URL to redirect to, as a string. Or ``None`` to raise a 410 (Gone)
HTTP error. HTTP error.
.. attribute:: pattern_name
The name of the URL pattern to redirect to. Reversing will be done
using the same args and kwargs as are passed in for this view.
.. attribute:: permanent .. attribute:: permanent
Whether the redirect should be permanent. The only difference here is Whether the redirect should be permanent. The only difference here is

View File

@ -63,7 +63,7 @@ ArchiveIndexView
month or day using the attribute ``date_list_period``. This also applies month or day using the attribute ``date_list_period``. This also applies
to all subclass views. to all subclass views.
**Example views.py**:: **Example myapp/views.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.views.generic.dates import ArchiveIndexView from django.views.generic.dates import ArchiveIndexView
@ -160,7 +160,7 @@ YearArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_year``. * Uses a default ``template_name_suffix`` of ``_archive_year``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.dates import YearArchiveView from django.views.generic.dates import YearArchiveView
@ -172,7 +172,7 @@ YearArchiveView
make_object_list = True make_object_list = True
allow_future = True allow_future = True
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -255,7 +255,7 @@ MonthArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_month``. * Uses a default ``template_name_suffix`` of ``_archive_month``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.dates import MonthArchiveView from django.views.generic.dates import MonthArchiveView
@ -267,7 +267,7 @@ MonthArchiveView
make_object_list = True make_object_list = True
allow_future = True allow_future = True
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -348,7 +348,7 @@ WeekArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_week``. * Uses a default ``template_name_suffix`` of ``_archive_week``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.dates import WeekArchiveView from django.views.generic.dates import WeekArchiveView
@ -361,7 +361,7 @@ WeekArchiveView
week_format = "%W" week_format = "%W"
allow_future = True allow_future = True
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -463,7 +463,7 @@ DayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_day``. * Uses a default ``template_name_suffix`` of ``_archive_day``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.dates import DayArchiveView from django.views.generic.dates import DayArchiveView
@ -475,7 +475,7 @@ DayArchiveView
make_object_list = True make_object_list = True
allow_future = True allow_future = True
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -537,7 +537,7 @@ TodayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_today``. * Uses a default ``template_name_suffix`` of ``_archive_today``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.dates import TodayArchiveView from django.views.generic.dates import TodayArchiveView
@ -549,7 +549,7 @@ TodayArchiveView
make_object_list = True make_object_list = True
allow_future = True allow_future = True
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -599,7 +599,7 @@ DateDetailView
* Uses a default ``template_name_suffix`` of ``_detail``. * Uses a default ``template_name_suffix`` of ``_detail``.
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.views.generic.dates import DateDetailView from django.views.generic.dates import DateDetailView

View File

@ -36,7 +36,7 @@ DetailView
9. ``get()`` 9. ``get()``
10. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` 10. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.utils import timezone from django.utils import timezone
@ -52,7 +52,7 @@ DetailView
context['now'] = timezone.now() context['now'] = timezone.now()
return context return context
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -62,6 +62,16 @@ DetailView
url(r'^(?P<slug>[-_\w]+)/$', ArticleDetailView.as_view(), name='article-detail'), url(r'^(?P<slug>[-_\w]+)/$', ArticleDetailView.as_view(), name='article-detail'),
) )
**Example myapp/article_detail.html**:
.. code-block:: html+django
<h1>{{ object.headline }}</h1>
<p>{{ object.content }}</p>
<p>Reporter: {{ object.reporter }}</p>
<p>Published: {{ object.pub_date|date }}</p>
<p>Date: {{ object.now|date }}</p>
ListView ListView
-------- --------
@ -111,7 +121,7 @@ ListView
context['now'] = timezone.now() context['now'] = timezone.now()
return context return context
**Example urls.py**:: **Example myapp/urls.py**::
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
@ -121,6 +131,19 @@ ListView
url(r'^$', ArticleListView.as_view(), name='article-list'), url(r'^$', ArticleListView.as_view(), name='article-list'),
) )
**Example myapp/article_list.html**:
.. code-block:: html+django
<h1>Articles</h1>
<ul>
{% for article in object_list %}
<li>{{ article.pub_date|date }} - {{ article.headline }}</li>
{% empty %}
<li>No articles yet.</li>
{% endfor %}
</ul>
.. class:: django.views.generic.list.BaseListView .. class:: django.views.generic.list.BaseListView
A base view for displaying a list of objects. It is not intended to be used A base view for displaying a list of objects. It is not intended to be used

View File

@ -42,7 +42,7 @@ FormView
* :class:`django.views.generic.edit.ProcessFormView` * :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View` * :class:`django.views.generic.base.View`
**Example forms.py**:: **Example myapp/forms.py**::
from django import forms from django import forms
@ -54,7 +54,7 @@ FormView
# send email using the self.cleaned_data dictionary # send email using the self.cleaned_data dictionary
pass pass
**Example views.py**:: **Example myapp/views.py**::
from myapp.forms import ContactForm from myapp.forms import ContactForm
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
@ -70,6 +70,16 @@ FormView
form.send_email() form.send_email()
return super(ContactView, self).form_valid(form) return super(ContactView, self).form_valid(form)
**Example myapp/contact.html**:
.. code-block:: html+django
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send message" />
</form>
CreateView CreateView
---------- ----------
@ -101,7 +111,7 @@ CreateView
creating objects for the example ``Author`` model would cause the creating objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_create_form.html'``. default ``template_name`` to be ``'myapp/author_create_form.html'``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from myapp.models import Author from myapp.models import Author
@ -110,6 +120,15 @@ CreateView
model = Author model = Author
fields = ['name'] fields = ['name']
**Example myapp/author_form.html**:
.. code-block:: html+django
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Create" />
</form>
UpdateView UpdateView
---------- ----------
@ -143,7 +162,7 @@ UpdateView
updating objects for the example ``Author`` model would cause the updating objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_update_form.html'``. default ``template_name`` to be ``'myapp/author_update_form.html'``.
**Example views.py**:: **Example myapp/views.py**::
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from myapp.models import Author from myapp.models import Author
@ -151,6 +170,16 @@ UpdateView
class AuthorUpdate(UpdateView): class AuthorUpdate(UpdateView):
model = Author model = Author
fields = ['name'] fields = ['name']
template_name_suffix = '_update_form'
**Example myapp/author_update_form.html**:
.. code-block:: html+django
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update" />
</form>
DeleteView DeleteView
---------- ----------
@ -184,8 +213,7 @@ DeleteView
deleting objects for the example ``Author`` model would cause the deleting objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_check_delete.html'``. default ``template_name`` to be ``'myapp/author_check_delete.html'``.
**Example myapp/views.py**::
**Example views.py**::
from django.views.generic.edit import DeleteView from django.views.generic.edit import DeleteView
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
@ -194,3 +222,12 @@ DeleteView
class AuthorDelete(DeleteView): class AuthorDelete(DeleteView):
model = Author model = Author
success_url = reverse_lazy('author-list') success_url = reverse_lazy('author-list')
**Example myapp/author_confirm_delete.html**:
.. code-block:: html+django
<form action="" method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm" />
</form>

View File

@ -870,6 +870,14 @@ subclass::
``prepopulated_fields`` doesn't accept ``DateTimeField``, ``ForeignKey``, ``prepopulated_fields`` doesn't accept ``DateTimeField``, ``ForeignKey``,
nor ``ManyToManyField`` fields. nor ``ManyToManyField`` fields.
.. attribute:: ModelAdmin.preserve_filters
.. versionadded:: 1.6
The admin now preserves filters on the list view after creating, editing
or deleting an object. You can restore the previous behavior of clearing
filters by setting this attribute to ``False``.
.. attribute:: ModelAdmin.radio_fields .. attribute:: ModelAdmin.radio_fields
By default, Django's admin uses a select-box interface (<select>) for By default, Django's admin uses a select-box interface (<select>) for
@ -2269,9 +2277,9 @@ your URLconf. Specifically, add these four patterns:
.. code-block:: python .. code-block:: python
url(r'^admin/password_reset/$', 'django.contrib.auth.views.password_reset', name='admin_password_reset'), url(r'^admin/password_reset/$', 'django.contrib.auth.views.password_reset', name='admin_password_reset'),
(r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done'), url(r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'),
(r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'), url(r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm', name='password_reset_confirm'),
(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete'), url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'),
(This assumes you've added the admin at ``admin/`` and requires that you put (This assumes you've added the admin at ``admin/`` and requires that you put
the URLs starting with ``^admin/`` before the line that includes the admin app the URLs starting with ``^admin/`` before the line that includes the admin app

View File

@ -132,12 +132,28 @@ Methods
password hashing. Doesn't save the password hashing. Doesn't save the
:class:`~django.contrib.auth.models.User` object. :class:`~django.contrib.auth.models.User` object.
When the ``raw_password`` is ``None``, the password will be set to an
unusable password, as if
:meth:`~django.contrib.auth.models.User.set_unusable_password()`
were used.
.. versionchanged:: 1.6
In Django 1.4 and 1.5, a blank string was unintentionally stored
as an unsable password.
.. method:: check_password(raw_password) .. method:: check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the the user. (This takes care of the password hashing in making the
comparison.) comparison.)
.. versionchanged:: 1.6
In Django 1.4 and 1.5, a blank string was unintentionally
considered to be an unusable password, resulting in this method
returning ``False`` for such a password.
.. method:: set_unusable_password() .. method:: set_unusable_password()
Marks the user as having no password set. This isn't the same as Marks the user as having no password set. This isn't the same as

View File

@ -47,7 +47,7 @@ Then either:
3. Add an entry in your URLconf. For example:: 3. Add an entry in your URLconf. For example::
urlpatterns = patterns('', urlpatterns = patterns('',
('^pages/', include('django.contrib.flatpages.urls')), (r'^pages/', include('django.contrib.flatpages.urls')),
) )
or: or:
@ -74,7 +74,7 @@ There are several ways to include the flat pages in your URLconf. You can
dedicate a particular path to flat pages:: dedicate a particular path to flat pages::
urlpatterns = patterns('', urlpatterns = patterns('',
('^pages/', include('django.contrib.flatpages.urls')), (r'^pages/', include('django.contrib.flatpages.urls')),
) )
You can also set it up as a "catchall" pattern. In this case, it is important You can also set it up as a "catchall" pattern. In this case, it is important
@ -82,9 +82,15 @@ to place the pattern at the end of the other urlpatterns::
# Your other patterns here # Your other patterns here
urlpatterns += patterns('django.contrib.flatpages.views', urlpatterns += patterns('django.contrib.flatpages.views',
(r'^(?P<url>.*)$', 'flatpage'), (r'^(?P<url>.*/)$', 'flatpage'),
) )
.. warning::
If you set :setting:`APPEND_SLASH` to ``False``, you must remove the slash
in the catchall pattern or flatpages without a trailing slash will not be
matched.
Another common setup is to use flat pages for a limited set of known pages and Another common setup is to use flat pages for a limited set of known pages and
to hard code the urls, so you can reference them with the :ttag:`url` template to hard code the urls, so you can reference them with the :ttag:`url` template
tag:: tag::

View File

@ -166,7 +166,7 @@ This template expects a ``wizard`` object that has various items attached to
it: it:
* ``form`` -- The :class:`~django.forms.Form` or * ``form`` -- The :class:`~django.forms.Form` or
:class:`~django.forms.formset.BaseFormSet` instance for the current step :class:`~django.forms.formsets.BaseFormSet` instance for the current step
(either empty or with errors). (either empty or with errors).
* ``steps`` -- A helper object to access the various steps related data: * ``steps`` -- A helper object to access the various steps related data:

View File

@ -89,6 +89,19 @@ documentation for the :djadminopt:`--verbosity` option.
Available commands Available commands
================== ==================
checksetup
----------
.. django-admin:: checksetup
.. versionadded:: 1.6
Performs a series of checks to verify a given setup (settings/application code)
is compatible with the current version of Django.
Upon finding things that are incompatible or require notifying the user, it
issues a series of warnings.
cleanup cleanup
------- -------

View File

@ -498,6 +498,8 @@ include ``%s`` -- then the library will act as if ``auto_id`` is ``True``.
By default, ``auto_id`` is set to the string ``'id_%s'``. By default, ``auto_id`` is set to the string ``'id_%s'``.
.. attribute:: Form.label_suffix
Normally, a colon (``:``) will be appended after any label name when a form is Normally, a colon (``:``) will be appended after any label name when a form is
rendered. It's possible to change the colon to another character, or omit it rendered. It's possible to change the colon to another character, or omit it
entirely, using the ``label_suffix`` parameter:: entirely, using the ``label_suffix`` parameter::
@ -650,12 +652,17 @@ To separately render the label tag of a form field, you can call its
>>> f = ContactForm(data) >>> f = ContactForm(data)
>>> print(f['message'].label_tag()) >>> print(f['message'].label_tag())
<label for="id_message">Message</label> <label for="id_message">Message:</label>
Optionally, you can provide the ``contents`` parameter which will replace the Optionally, you can provide the ``contents`` parameter which will replace the
auto-generated label tag. An optional ``attrs`` dictionary may contain auto-generated label tag. An optional ``attrs`` dictionary may contain
additional attributes for the ``<label>`` tag. additional attributes for the ``<label>`` tag.
.. versionchanged:: 1.6
The label now includes the form's :attr:`~django.forms.Form.label_suffix`
(a semicolon, by default).
.. method:: BoundField.css_classes() .. method:: BoundField.css_classes()
When you use Django's rendering shortcuts, CSS classes are used to When you use Django's rendering shortcuts, CSS classes are used to
@ -688,6 +695,29 @@ by a ``Widget``::
>>> print(bound_form['subject'].value()) >>> print(bound_form['subject'].value())
hi hi
.. attribute:: BoundField.id_for_label
Use this property to render the ID of this field. For example, if you are
manually constructing a ``<label>`` in your template (despite the fact that
:meth:`~BoundField.label_tag` will do this for you):
.. code-block:: html+django
<label for="{{ form.my_field.id_for_label }}">...</label>{{ my_field }}
By default, this will be the field's name prefixed by ``id_``
("``id_my_field``" for the example above). You may modify the ID by setting
:attr:`~django.forms.Widget.attrs` on the field's widget. For example,
declaring a field like this::
my_field = forms.CharField(widget=forms.TextInput(attrs={'id': 'myFIELD'}))
and using the template above, would render something like:
.. code-block:: html
<label for="myFIELD">...</label><input id="myFIELD" type="text" name="my_field" />
.. _binding-uploaded-files: .. _binding-uploaded-files:
Binding uploaded files to a form Binding uploaded files to a form

View File

@ -5,7 +5,7 @@ Model Form Functions
.. module:: django.forms.models .. module:: django.forms.models
:synopsis: Django's functions for building model forms and formsets. :synopsis: Django's functions for building model forms and formsets.
.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None) .. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None)
Returns a :class:`~django.forms.ModelForm` class for the given ``model``. Returns a :class:`~django.forms.ModelForm` class for the given ``model``.
You can optionally pass a ``form`` argument to use as a starting point for You can optionally pass a ``form`` argument to use as a starting point for
@ -20,11 +20,18 @@ Model Form Functions
``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.
``formfield_callback`` is a callable that takes a model field and returns ``formfield_callback`` is a callable that takes a model field and returns
a form field. a form field.
``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.
See :ref:`modelforms-factory` for example usage. See :ref:`modelforms-factory` for example usage.
.. versionchanged:: 1.6 .. versionchanged:: 1.6
@ -35,14 +42,16 @@ Model Form Functions
information. Omitting any definition of the fields to use will result in all information. Omitting any definition of the fields to use will result in all
fields being used, but this behavior is deprecated. fields being used, but this behavior is deprecated.
The ``localized_fields`` parameter was added. The ``localized_fields``, ``labels``, ``help_texts``, and
``error_messages`` parameters were added.
.. function:: 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) .. function:: 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, labels=None, help_texts=None, error_messages=None)
Returns a ``FormSet`` class for the given ``model`` class. Returns a ``FormSet`` class for the given ``model`` class.
Arguments ``model``, ``form``, ``fields``, ``exclude``, Arguments ``model``, ``form``, ``fields``, ``exclude``,
``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``,
``help_texts``, and ``error_messages`` are all passed through to
:func:`~django.forms.models.modelform_factory`. :func:`~django.forms.models.modelform_factory`.
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@ -54,9 +63,10 @@ Model Form Functions
.. versionchanged:: 1.6 .. versionchanged:: 1.6
The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. The ``widgets``, ``validate_max``, ``localized_fields``, ``labels``,
``help_texts``, and ``error_messages`` parameters were added.
.. function:: 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) .. function:: 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, labels=None, help_texts=None, error_messages=None)
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@ -69,4 +79,5 @@ Model Form Functions
.. versionchanged:: 1.6 .. versionchanged:: 1.6
The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. The ``widgets``, ``validate_max`` and ``localized_fields``, ``labels``,
``help_texts``, and ``error_messages`` parameters were added.

View File

@ -12,13 +12,11 @@ validation (accessing the ``errors`` attribute or calling ``full_clean()``
directly), but normally they won't be needed. directly), but normally they won't be needed.
In general, any cleaning method can raise ``ValidationError`` if there is a In general, any cleaning method can raise ``ValidationError`` if there is a
problem with the data it is processing, passing the relevant error message to problem with the data it is processing, passing the relevant information to
the ``ValidationError`` constructor. If no ``ValidationError`` is raised, the the ``ValidationError`` constructor. :ref:`See below <raising-validation-error>`
method should return the cleaned (normalized) data as a Python object. for the best practice in raising ``ValidationError``. If no ``ValidationError``
is raised, the method should return the cleaned (normalized) data as a Python
If you detect multiple errors during a cleaning method and wish to signal all object.
of them to the form submitter, it is possible to pass a list of errors to the
``ValidationError`` constructor.
Most validation can be done using `validators`_ - simple helpers that can be Most validation can be done using `validators`_ - simple helpers that can be
reused easily. Validators are simple functions (or callables) that take a single reused easily. Validators are simple functions (or callables) that take a single
@ -87,7 +85,8 @@ overridden:
"field" (called ``__all__``), which you can access via the "field" (called ``__all__``), which you can access via the
``non_field_errors()`` method if you need to. If you want to attach ``non_field_errors()`` method if you need to. If you want to attach
errors to a specific field in the form, you will need to access the errors to a specific field in the form, you will need to access the
``_errors`` attribute on the form, which is `described later`_. ``_errors`` attribute on the form, which is
:ref:`described later <modifying-field-errors>`.
Also note that there are special considerations when overriding Also note that there are special considerations when overriding
the ``clean()`` method of a ``ModelForm`` subclass. (see the the ``clean()`` method of a ``ModelForm`` subclass. (see the
@ -116,7 +115,100 @@ should iterate through ``self.cleaned_data.items()``, possibly considering the
``_errors`` dictionary attribute on the form as well. In this way, you will ``_errors`` dictionary attribute on the form as well. In this way, you will
already know which fields have passed their individual validation requirements. already know which fields have passed their individual validation requirements.
.. _described later: .. _raising-validation-error:
Raising ``ValidationError``
---------------------------
.. versionchanged:: 1.6
In order to make error messages flexible and easy to override, consider the
following guidelines:
* Provide a descriptive error ``code`` to the constructor::
# Good
ValidationError(_('Invalid value'), code='invalid')
# Bad
ValidationError(_('Invalid value'))
* Don't coerce variables into the message; use placeholders and the ``params``
argument of the constructor::
# Good
ValidationError(
_('Invalid value: %(value)s'),
params={'value': '42'},
)
# Bad
ValidationError(_('Invalid value: %s') % value)
* Use mapping keys instead of positional formatting. This enables putting
the variables in any order or omitting them altogether when rewriting the
message::
# Good
ValidationError(
_('Invalid value: %(value)s'),
params={'value': '42'},
)
# Bad
ValidationError(
_('Invalid value: %s'),
params=('42',),
)
* Wrap the message with ``gettext`` to enable translation::
# Good
ValidationError(_('Invalid value'))
# Bad
ValidationError('Invalid value')
Putting it all together::
raise ValidationErrror(
_('Invalid value: %(value)s'),
code='invalid',
params={'value': '42'},
)
Following these guidelines is particularly necessary if you write reusable
forms, form fields, and model fields.
While not recommended, if you are at the end of the validation chain
(i.e. your form ``clean()`` method) and you know you will *never* need
to override your error message you can still opt for the less verbose::
ValidationError(_('Invalid value: %s') % value)
Raising multiple errors
~~~~~~~~~~~~~~~~~~~~~~~
If you detect multiple errors during a cleaning method and wish to signal all
of them to the form submitter, it is possible to pass a list of errors to the
``ValidationError`` constructor.
As above, it is recommended to pass a list of ``ValidationError`` instances
with ``code``\s and ``params`` but a list of strings will also work::
# Good
raise ValidationError([
ValidationError(_('Error 1'), code='error1'),
ValidationError(_('Error 2'), code='error2'),
])
# Bad
raise ValidationError([
_('Error 1'),
_('Error 2'),
])
.. _modifying-field-errors:
Form subclasses and modifying field errors Form subclasses and modifying field errors
------------------------------------------ ------------------------------------------

View File

@ -163,6 +163,9 @@ Django will then include the extra attributes in the rendered output:
<tr><th>Url:</th><td><input type="url" name="url"/></td></tr> <tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
You can also set the HTML ``id`` using :attr:`~Widget.attrs`. See
:attr:`BoundField.id_for_label` for an example.
.. _styling-widget-classes: .. _styling-widget-classes:
Styling widget classes Styling widget classes
@ -251,7 +254,7 @@ foundation for custom widgets.
into two separate values:: into two separate values::
from django.forms import MultiWidget from django.forms import MultiWidget
class SplitDateTimeWidget(MultiWidget): class SplitDateTimeWidget(MultiWidget):
# ... # ...

View File

@ -84,12 +84,18 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle
validation errors yourself, or if you have excluded fields from the validation errors yourself, or if you have excluded fields from the
:class:`~django.forms.ModelForm` that require validation. :class:`~django.forms.ModelForm` that require validation.
.. method:: Model.full_clean(exclude=None) .. method:: Model.full_clean(exclude=None, validate_unique=True)
.. versionchanged:: 1.6
The ``validate_unique`` parameter was added to allow skipping
:meth:`Model.validate_unique()`. Previously, :meth:`Model.validate_unique()`
was always called by ``full_clean``.
This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and
:meth:`Model.validate_unique()`, in that order and raises a :meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``, in that
:exc:`~django.core.exceptions.ValidationError` that has a ``message_dict`` order and raises a :exc:`~django.core.exceptions.ValidationError` that has a
attribute containing errors from all three stages. ``message_dict`` attribute containing errors from all three stages.
The optional ``exclude`` argument can be used to provide a list of field names The optional ``exclude`` argument can be used to provide a list of field names
that can be excluded from validation and cleaning. that can be excluded from validation and cleaning.

View File

@ -852,6 +852,10 @@ It is also important to remember that when running with :setting:`DEBUG`
turned on, Django will remember every SQL query it executes. This is useful turned on, Django will remember every SQL query it executes. This is useful
when you're debugging, but it'll rapidly consume memory on a production server. when you're debugging, but it'll rapidly consume memory on a production server.
Finally, if :setting:`DEBUG` is ``False``, you also need to properly set
the :setting:`ALLOWED_HOSTS` setting. Failing to do so will result in all
requests being returned as "Bad Request (400)".
.. _django/views/debug.py: https://github.com/django/django/blob/master/django/views/debug.py .. _django/views/debug.py: https://github.com/django/django/blob/master/django/views/debug.py
.. setting:: DEBUG_PROPAGATE_EXCEPTIONS .. setting:: DEBUG_PROPAGATE_EXCEPTIONS

View File

@ -498,17 +498,20 @@ request_finished
Sent when Django finishes processing an HTTP request. Sent when Django finishes processing an HTTP request.
.. note::
When a view returns a :ref:`streaming response <httpresponse-streaming>`,
this signal is sent only after the entire response is consumed by the
client (strictly speaking, by the WSGI gateway).
.. versionchanged:: 1.5 .. versionchanged:: 1.5
Before Django 1.5, this signal was fired before sending the content to the Before Django 1.5, this signal was sent before delivering content to the
client. In order to accomodate streaming responses, it is now fired after client. In order to accommodate :ref:`streaming responses
sending the content. <httpresponse-streaming>`, it is now sent after the response has been fully
delivered to the client.
.. note::
Some WSGI servers and middleware do not always call ``close`` on the
response object after handling a request, most notably uWSGI prior to 1.2.6
and Sentry's error reporting middleware up to 2.0.7. In those cases this
signal isn't sent at all. This can result in idle connections to database
and memcache servers.
Arguments sent with this signal: Arguments sent with this signal:

View File

@ -371,7 +371,7 @@ displayed if the given array is empty or could not be found::
{% for athlete in athlete_list %} {% for athlete in athlete_list %}
<li>{{ athlete.name }}</li> <li>{{ athlete.name }}</li>
{% empty %} {% empty %}
<li>Sorry, no athlete in this list!</li> <li>Sorry, no athletes in this list.</li>
{% endfor %} {% endfor %}
<ul> <ul>
@ -2401,7 +2401,7 @@ It is also able to consume standard context variables, e.g. assuming a
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" /> <link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
If you'd like to retrieve a static URL without displaying it, you can use a If you'd like to retrieve a static URL without displaying it, you can use a
slightly different call:: slightly different call:
.. versionadded:: 1.5 .. versionadded:: 1.5

View File

@ -659,6 +659,8 @@ Functions for working with Python modules.
.. function:: import_by_path(dotted_path, error_prefix='') .. function:: import_by_path(dotted_path, error_prefix='')
.. versionadded:: 1.6
Imports a dotted module path and returns the attribute/class designated by Imports a dotted module path and returns the attribute/class designated by
the last name in the path. Raises the last name in the path. Raises
:exc:`~django.core.exceptions.ImproperlyConfigured` if something goes :exc:`~django.core.exceptions.ImproperlyConfigured` if something goes

View File

@ -440,7 +440,15 @@ generation.
This signal is now sent after the content is fully consumed by the WSGI This signal is now sent after the content is fully consumed by the WSGI
gateway. This might be backwards incompatible if you rely on the signal being gateway. This might be backwards incompatible if you rely on the signal being
fired before sending the response content to the client. If you do, you should fired before sending the response content to the client. If you do, you should
consider using a middleware instead. consider using :doc:`middleware </topics/http/middleware>` instead.
.. note::
Some WSGI servers and middleware do not always call ``close`` on the
response object after handling a request, most notably uWSGI prior to 1.2.6
and Sentry's error reporting middleware up to 2.0.7. In those cases the
``request_finished`` signal isn't sent at all. This can result in idle
connections to database and memcache servers.
OPTIONS, PUT and DELETE requests in the test client OPTIONS, PUT and DELETE requests in the test client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -121,6 +121,13 @@ GeoDjango now provides :ref:`form fields and widgets <ref-gis-forms-api>` for
its geo-specialized fields. They are OpenLayers-based by default, but they can its geo-specialized fields. They are OpenLayers-based by default, but they can
be customized to use any other JS framework. be customized to use any other JS framework.
``checksetup`` management command added for verifying compatibility
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A ``checksetup`` management command was added, enabling you to verify if your
current configuration (currently oriented at settings) is compatible with the
current version of Django.
Minor features Minor features
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
@ -236,9 +243,14 @@ Minor features
.. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`Pillow`: https://pypi.python.org/pypi/Pillow
.. _`PIL`: https://pypi.python.org/pypi/PIL .. _`PIL`: https://pypi.python.org/pypi/PIL
* :doc:`ModelForm </topics/forms/modelforms/>` accepts a new * :class:`~django.forms.ModelForm` accepts several new ``Meta``
Meta option: ``localized_fields``. Fields included in this list will be localized options.
(by setting ``localize`` on the form field).
* Fields included in the ``localized_fields`` list will be localized
(by setting ``localize`` on the form field).
* The ``labels``, ``help_texts`` and ``error_messages`` options may be used
to customize the default fields, see
:ref:`modelforms-overriding-default-fields` for details.
* The ``choices`` argument to model fields now accepts an iterable of iterables * The ``choices`` argument to model fields now accepts an iterable of iterables
instead of requiring an iterable of lists or tuples. instead of requiring an iterable of lists or tuples.
@ -303,6 +315,21 @@ Minor features
:class:`~django.contrib.admin.InlineModelAdmin` may be overridden to :class:`~django.contrib.admin.InlineModelAdmin` may be overridden to
customize the extra and maximum number of inline forms. customize the extra and maximum number of inline forms.
* Formsets now have a
:meth:`~django.forms.formsets.BaseFormSet.total_error_count` method.
* :class:`~django.forms.ModelForm` fields can now override error messages
defined in model fields by using the
:attr:`~django.forms.Field.error_messages` argument of a ``Field``'s
constructor. To take advantage of this new feature with your custom fields,
:ref:`see the updated recommendation <raising-validation-error>` for raising
a ``ValidationError``.
* :class:`~django.contrib.admin.ModelAdmin` now preserves filters on the list view
after creating, editing or deleting an object. It's possible to restore the previous
behavior of clearing filters by setting the
:attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``.
Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================
@ -581,6 +608,47 @@ It is still possible to convert the fetched rows to ``Model`` objects
lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()` lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()`
method. method.
:meth:`BoundField.label_tag<django.forms.BoundField.label_tag>` now includes the form's :attr:`~django.forms.Form.label_suffix`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is consistent with how methods like
:meth:`Form.as_p<django.forms.Form.as_p>` and
:meth:`Form.as_ul<django.forms.Form.as_ul>` render labels.
If you manually render ``label_tag`` in your templates:
.. code-block:: html+django
{{ form.my_field.label_tag }}: {{ form.my_field }}
you'll want to remove the semicolon (or whatever other separator you may be
using) to avoid duplicating it when upgrading to Django 1.6. The following
template in Django 1.6 will render identically to the above template in Django
1.5, except that the semicolon will appear inside the ``<label>`` element.
.. code-block:: html+django
{{ form.my_field.label_tag }} {{ form.my_field }}
will render something like:
.. code-block:: html
<label for="id_my_field">My Field:</label> <input id="id_my_field" type="text" name="my_field" />
If you want to keep the current behavior of rendering ``label_tag`` without
the ``label_suffix``, instantiate the form ``label_suffix=''``.
Admin views ``_changelist_filters`` GET parameter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To achieve preserving and restoring list view filters, admin views now
pass around the `_changelist_filters` GET parameter. It's important that you
account for that change if you have custom admin templates or if your tests
rely on the previous URLs. If you want to revert to the original behavior you
can set the
:attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``.
Miscellaneous Miscellaneous
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -634,6 +702,29 @@ Miscellaneous
:meth:`django.contrib.auth.logout` which will send the :meth:`django.contrib.auth.logout` which will send the
:func:`~django.contrib.auth.signals.user_logged_out` signal. :func:`~django.contrib.auth.signals.user_logged_out` signal.
* :ref:`Authentication views <built-in-auth-views>` are now reversed by name,
not their locations in ``django.contrib.auth.views``. If you are using the
views without a ``name``, you should update your ``urlpatterns`` to use
:meth:`~django.conf.urls.url` with the ``name`` parameter. For example::
(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete')
becomes::
url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete')
* :class:`~django.views.generic.base.RedirectView` now has a `pattern_name`
attribute which allows it to choose the target by reversing the URL.
* In Django 1.4 and 1.5, a blank string was unintentionally not considered to
be a valid password. This meant
:meth:`~django.contrib.auth.models.User.set_password()` would save a blank
password as an unusable password like
:meth:`~django.contrib.auth.models.User.set_unusable_password()` does, and
thus :meth:`~django.contrib.auth.models.User.check_password()` always
returned ``False`` for blank passwords. This has been corrected in this
release: blank passwords are now valid.
Features deprecated in 1.6 Features deprecated in 1.6
========================== ==========================

View File

@ -37,7 +37,7 @@ plug in other authentication sources. You can override Django's default
database-based scheme, or you can use the default system in tandem with other database-based scheme, or you can use the default system in tandem with other
systems. systems.
See the `authentication backend reference See the :ref:`authentication backend reference
<authentication-backends-reference>` for information on the authentication <authentication-backends-reference>` for information on the authentication
backends included with Django. backends included with Django.
@ -583,12 +583,28 @@ The following methods are available on any subclass of
password hashing. Doesn't save the password hashing. Doesn't save the
:class:`~django.contrib.auth.models.AbstractBaseUser` object. :class:`~django.contrib.auth.models.AbstractBaseUser` object.
When the raw_password is ``None``, the password will be set to an
unusable password, as if
:meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()`
were used.
.. versionchanged:: 1.6
In Django 1.4 and 1.5, a blank string was unintentionally stored
as an unsable password as well.
.. method:: models.AbstractBaseUser.check_password(raw_password) .. method:: models.AbstractBaseUser.check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the the user. (This takes care of the password hashing in making the
comparison.) comparison.)
.. versionchanged:: 1.6
In Django 1.4 and 1.5, a blank string was unintentionally
considered to be an unusable password, resulting in this method
returning ``False`` for such a password.
.. method:: models.AbstractBaseUser.set_unusable_password() .. method:: models.AbstractBaseUser.set_unusable_password()
Marks the user as having no password set. This isn't the same as Marks the user as having no password set. This isn't the same as

View File

@ -206,6 +206,12 @@ from the ``User`` model.
database to check against, and returns ``True`` if they match, ``False`` database to check against, and returns ``True`` if they match, ``False``
otherwise. otherwise.
.. versionchanged:: 1.6
In Django 1.4 and 1.5, a blank string was unintentionally considered
to be an unusable password, resulting in this method returning
``False`` for such a password.
.. function:: make_password(password[, salt, hashers]) .. function:: make_password(password[, salt, hashers])
Creates a hashed password in the format used by this application. It takes Creates a hashed password in the format used by this application. It takes

View File

@ -233,7 +233,7 @@ We'll demonstrate this with the publisher modelling we used in the
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from books.models import Author from books.models import Author
class RecordInterest(View, SingleObjectMixin): class RecordInterest(SingleObjectMixin, View):
"""Records the current user's interest in an author.""" """Records the current user's interest in an author."""
model = Author model = Author
@ -446,7 +446,7 @@ Our new ``AuthorDetail`` looks like this::
class AuthorInterestForm(forms.Form): class AuthorInterestForm(forms.Form):
message = forms.CharField() message = forms.CharField()
class AuthorDetail(DetailView, FormMixin): class AuthorDetail(FormMixin, DetailView):
model = Author model = Author
form_class = AuthorInterestForm form_class = AuthorInterestForm
@ -553,7 +553,7 @@ template as ``AuthorDisplay`` is using on ``GET``.
from django.views.generic import FormView from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
class AuthorInterest(FormView, SingleObjectMixin): class AuthorInterest(SingleObjectMixin, FormView):
template_name = 'books/author_detail.html' template_name = 'books/author_detail.html'
form_class = AuthorInterestForm form_class = AuthorInterestForm
model = Author model = Author

View File

@ -87,9 +87,9 @@ The following approach may be used to close files automatically::
# Create a Python file object using open() and the with statement # Create a Python file object using open() and the with statement
>>> with open('/tmp/hello.world', 'w') as f: >>> with open('/tmp/hello.world', 'w') as f:
>>> myfile = File(f) ... myfile = File(f)
>>> for line in myfile: ... myfile.write('Hello World')
>>> print line ...
>>> myfile.closed >>> myfile.closed
True True
>>> f.closed >>> f.closed

View File

@ -3,7 +3,7 @@
Formsets Formsets
======== ========
.. class:: django.forms.formset.BaseFormSet .. class:: django.forms.formsets.BaseFormSet
A formset is a layer of abstraction to work with multiple forms on the same A formset is a layer of abstraction to work with multiple forms on the same
page. It can be best compared to a data grid. Let's say you have the following page. It can be best compared to a data grid. Let's say you have the following
@ -164,6 +164,23 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the
forms in the formset. Validation was performed for each of the two forms, and forms in the formset. Validation was performed for each of the two forms, and
the expected error message appears for the second item. the expected error message appears for the second item.
.. currentmodule:: django.forms.formsets.BaseFormSet
.. method:: total_error_count(self)
.. versionadded:: 1.6
To check how many errors there are in the formset, we can use the
``total_error_count`` method::
>>> # Using the previous example
>>> formset.errors
[{}, {'pub_date': [u'This field is required.']}]
>>> len(formset.errors)
2
>>> formset.total_error_count()
1
We can also check if form data differs from the initial data (i.e. the form was We can also check if form data differs from the initial data (i.e. the form was
sent without any data):: sent without any data)::
@ -247,8 +264,7 @@ is where you define your own validation that works at the formset level::
... # Don't bother validating the formset unless each form is valid on its own ... # Don't bother validating the formset unless each form is valid on its own
... return ... return
... titles = [] ... titles = []
... for i in range(0, self.total_form_count()): ... for form in self.forms:
... form = self.forms[i]
... title = form.cleaned_data['title'] ... title = form.cleaned_data['title']
... if title in titles: ... if title in titles:
... raise forms.ValidationError("Articles in a set must have distinct titles.") ... raise forms.ValidationError("Articles in a set must have distinct titles.")

View File

@ -302,7 +302,7 @@ loop::
{% for field in form %} {% for field in form %}
<div class="fieldWrapper"> <div class="fieldWrapper">
{{ field.errors }} {{ field.errors }}
{{ field.label_tag }}: {{ field }} {{ field.label_tag }} {{ field }}
</div> </div>
{% endfor %} {% endfor %}
<p><input type="submit" value="Send message" /></p> <p><input type="submit" value="Send message" /></p>
@ -316,8 +316,14 @@ attributes, which can be useful in your templates:
The label of the field, e.g. ``Email address``. The label of the field, e.g. ``Email address``.
``{{ field.label_tag }}`` ``{{ field.label_tag }}``
The field's label wrapped in the appropriate HTML ``<label>`` tag, The field's label wrapped in the appropriate HTML ``<label>`` tag.
e.g. ``<label for="id_email">Email address</label>``
.. versionchanged:: 1.6
This includes the form's :attr:`~django.forms.Form.label_suffix`. For
example, the default ``label_suffix`` is a semicolon::
<label for="id_email">Email address:</label>
``{{ field.value }}`` ``{{ field.value }}``
The value of the field. e.g ``someone@example.com`` The value of the field. e.g ``someone@example.com``
@ -375,7 +381,7 @@ these two methods::
{% for field in form.visible_fields %} {% for field in form.visible_fields %}
<div class="fieldWrapper"> <div class="fieldWrapper">
{{ field.errors }} {{ field.errors }}
{{ field.label_tag }}: {{ field }} {{ field.label_tag }} {{ field }}
</div> </div>
{% endfor %} {% endfor %}
<p><input type="submit" value="Send message" /></p> <p><input type="submit" value="Send message" /></p>
@ -403,7 +409,7 @@ using the :ttag:`include` tag to reuse it in other templates::
{% for field in form %} {% for field in form %}
<div class="fieldWrapper"> <div class="fieldWrapper">
{{ field.errors }} {{ field.errors }}
{{ field.label_tag }}: {{ field }} {{ field.label_tag }} {{ field }}
</div> </div>
{% endfor %} {% endfor %}

View File

@ -49,7 +49,7 @@ define the media requirements.
Here's a simple example:: Here's a simple example::
from django import froms from django import forms
class CalendarWidget(forms.TextInput): class CalendarWidget(forms.TextInput):
class Media: class Media:

View File

@ -141,7 +141,7 @@ In addition, each generated form field has attributes set as follows:
``default`` value will be initially selected instead). ``default`` value will be initially selected instead).
Finally, note that you can override the form field used for a given model Finally, note that you can override the form field used for a given model
field. See `Overriding the default field types or widgets`_ below. field. See `Overriding the default fields`_ below.
A full example A full example
-------------- --------------
@ -388,8 +388,10 @@ include that field.
.. _section on saving forms: `The save() method`_ .. _section on saving forms: `The save() method`_
Overriding the default field types or widgets .. _modelforms-overriding-default-fields:
---------------------------------------------
Overriding the default fields
-----------------------------
The default field types, as described in the `Field types`_ table above, are The default field types, as described in the `Field types`_ table above, are
sensible defaults. If you have a ``DateField`` in your model, chances are you'd sensible defaults. If you have a ``DateField`` in your model, chances are you'd
@ -420,38 +422,65 @@ widget::
The ``widgets`` dictionary accepts either widget instances (e.g., The ``widgets`` dictionary accepts either widget instances (e.g.,
``Textarea(...)``) or classes (e.g., ``Textarea``). ``Textarea(...)``) or classes (e.g., ``Textarea``).
If you want to further customize a field -- including its type, label, etc. -- .. versionadded:: 1.6
you can do this by declaratively specifying fields like you would in a regular
``Form``. Declared fields will override the default ones generated by using the
``model`` attribute.
For example, if you wanted to use ``MyDateFormField`` for the ``pub_date`` The ``labels``, ``help_texts`` and ``error_messages`` options were added.
Similarly, you can specify the ``labels``, ``help_texts`` and ``error_messages``
attributes of the inner ``Meta`` class if you want to further customize a field.
For example if you wanted to customize the wording of all user facing strings for
the ``name`` field::
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
labels = {
'name': _('Writer'),
}
help_texts = {
'name': _('Some useful help text.'),
}
error_messages = {
'name': {
'max_length': _("This writer's name is too long."),
},
}
Finally, if you want complete control over of a field -- including its type,
validators, etc. -- you can do this by declaratively specifying fields like you
would in a regular ``Form``. Declared fields will override the default ones
generated by using the ``model`` attribute. Fields declared like this will
ignore any customizations in the ``widgets``, ``labels``, ``help_texts``, and
``error_messages`` options declared on ``Meta``.
For example, if you wanted to use ``MySlugFormField`` for the ``slug``
field, you could do the following:: field, you could do the following::
from django.forms import ModelForm from django.forms import ModelForm
from myapp.models import Article from myapp.models import Article
class ArticleForm(ModelForm): class ArticleForm(ModelForm):
pub_date = MyDateFormField() slug = MySlugFormField()
class Meta: class Meta:
model = Article model = Article
fields = ['pub_date', 'headline', 'content', 'reporter'] fields = ['pub_date', 'headline', 'content', 'reporter']
If you want to override a field's default label, then specify the ``label`` If you want to override a field's default validators, then specify the
parameter when declaring the form field:: ``validators`` parameter when declaring the form field::
from django.forms import ModelForm, DateField from django.forms import ModelForm, DateField
from myapp.models import Article from myapp.models import Article
class ArticleForm(ModelForm): class ArticleForm(ModelForm):
pub_date = DateField(label='Publication date') slug = CharField(validators=[validate_slug])
class Meta: class Meta:
model = Article model = Article
fields = ['pub_date', 'headline', 'content', 'reporter'] fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
.. note:: .. note::
@ -597,7 +626,7 @@ example by specifying the widgets to be used for a given field::
>>> from django.forms import Textarea >>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm, >>> Form = modelform_factory(Book, form=BookForm,
widgets={"title": Textarea()}) ... widgets={"title": Textarea()})
The fields to include can be specified using the ``fields`` and ``exclude`` The fields to include can be specified using the ``fields`` and ``exclude``
keyword arguments, or the corresponding attributes on the ``ModelForm`` inner keyword arguments, or the corresponding attributes on the ``ModelForm`` inner
@ -907,7 +936,7 @@ Third, you can manually render each field::
{{ formset.management_form }} {{ formset.management_form }}
{% for form in formset %} {% for form in formset %}
{% for field in form %} {% for field in form %}
{{ field.label_tag }}: {{ field }} {{ field.label_tag }} {{ field }}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</form> </form>

View File

@ -17,6 +17,11 @@ Here are a couple of example settings::
DEFAULT_FROM_EMAIL = 'webmaster@example.com' DEFAULT_FROM_EMAIL = 'webmaster@example.com'
TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john') TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john')
.. note::
If you set :setting:`DEBUG` to ``False``, you also need to properly set
the :setting:`ALLOWED_HOSTS` setting.
Because a settings file is a Python module, the following apply: Because a settings file is a Python module, the following apply:
* It doesn't allow for Python syntax errors. * It doesn't allow for Python syntax errors.

View File

@ -155,6 +155,80 @@ If there are any circular dependencies in the
:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured`` :setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
exception will be raised. exception will be raised.
Advanced features of ``TransactionTestCase``
============================================
.. currentmodule:: django.test
.. attribute:: TransactionTestCase.available_apps
.. versionadded:: 1.6
.. warning::
This attribute is a private API. It may be changed or removed without
a deprecation period in the future, for instance to accomodate changes
in application loading.
It's used to optimize Django's own test suite, which contains hundreds
of models but no relations between models in different applications.
By default, ``available_apps`` is set to ``None``. After each test, Django
calls :djadmin:`flush` to reset the database state. This empties all tables
and emits the :data:`~django.db.models.signals.post_syncdb` signal, which
re-creates one content type and three permissions for each model. This
operation gets expensive proportionally to the number of models.
Setting ``available_apps`` to a list of applications instructs Django to
behave as if only the models from these applications were available. The
behavior of ``TransactionTestCase`` changes as follows:
- :data:`~django.db.models.signals.post_syncdb` is fired before each
test to create the content types and permissions for each model in
available apps, in case they're missing.
- After each test, Django empties only tables corresponding to models in
available apps. However, at the database level, truncation may cascade to
related models in unavailable apps. Furthermore
:data:`~django.db.models.signals.post_syncdb` isn't fired; it will be
fired by the next ``TransactionTestCase``, after the correct set of
applications is selected.
Since the database isn't fully flushed, if a test creates instances of
models not included in ``available_apps``, they will leak and they may
cause unrelated tests to fail. Be careful with tests that use sessions;
the default session engine stores them in the database.
Since :data:`~django.db.models.signals.post_syncdb` isn't emitted after
flushing the database, its state after a ``TransactionTestCase`` isn't the
same as after a ``TestCase``: it's missing the rows created by listeners
to :data:`~django.db.models.signals.post_syncdb`. Considering the
:ref:`order in which tests are executed <order-of-tests>`, this isn't an
issue, provided either all ``TransactionTestCase`` in a given test suite
declare ``available_apps``, or none of them.
``available_apps`` is mandatory in Django's own test suite.
.. attribute:: TransactionTestCase.reset_sequences
.. versionadded:: 1.5
Setting ``reset_sequences = True`` on a ``TransactionTestCase`` will make
sure sequences are always reset before the test run::
class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
reset_sequences = True
def test_animal_pk(self):
lion = Animal.objects.create(name="lion", sound="roar")
# lion.pk is guaranteed to always be 1
self.assertEqual(lion.pk, 1)
Unless you are explicitly testing primary keys sequence numbers, it is
recommended that you do not hard code primary key values in tests.
Using ``reset_sequences = True`` will slow down the test, since the primary
key reset is an relatively expensive database operation.
Running tests outside the test runner Running tests outside the test runner
===================================== =====================================

Some files were not shown because too many files have changed in this diff Show More