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
dist/
docs/_build/
docs/locale/
tests/coverage_html/
tests/.coverage

View File

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

View File

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

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

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static admin_modify %}
{% load admin_urls %}
{% load i18n admin_urls admin_static admin_modify %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
@ -29,7 +28,10 @@
{% if change %}{% if not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
<li><a href="{% url opts|admin_urlname:'history' original.pk|admin_urlquote %}" class="historylink">{% trans "History" %}</a></li>
<li>
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
</li>
{% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %}
</ul>

View File

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
{% load i18n admin_static admin_list %}
{% load admin_urls %}
{% load i18n admin_urls admin_static admin_list %}
{% block extrastyle %}
{{ block.super }}
@ -54,7 +53,8 @@
<ul class="object-tools">
{% block object-tools-items %}
<li>
<a href="{% url cl.opts|admin_urlname:'add' %}{% if is_popup %}?_popup=1{% endif %}" class="addlink">
{% url cl.opts|admin_urlname:'add' as add_url %}
<a href="{% add_preserved_filters add_url is_popup %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
@ -64,7 +64,7 @@
{% endblock %}
{% if cl.formset.errors %}
<p class="errornote">
{% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
{% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{{ cl.formset.non_form_errors }}
{% endif %}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,10 @@
{% load i18n admin_urls %}
<div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
{% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
{% if show_delete_link %}
{% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
<p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}

View File

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

View File

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

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.contrib.admin.util import quote
from django.core.urlresolvers import resolve, Resolver404
register = template.Library()
@register.filter
def admin_urlname(value, arg):
return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg)
@ -11,3 +20,36 @@ def admin_urlname(value, arg):
@register.filter
def admin_urlquote(value):
return quote(value)
@register.simple_tag(takes_context=True)
def add_preserved_filters(context, url, popup=False):
opts = context.get('opts')
preserved_filters = context.get('preserved_filters')
parsed_url = list(urlparse(url))
parsed_qs = dict(parse_qsl(parsed_url[4]))
merged_qs = dict()
if opts and preserved_filters:
preserved_filters = dict(parse_qsl(preserved_filters))
try:
match = resolve(url)
except Resolver404:
pass
else:
current_url = '%s:%s' % (match.namespace, match.url_name)
changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
if changelist_url == current_url and '_changelist_filters' in preserved_filters:
preserved_filters = dict(parse_qsl(preserved_filters['_changelist_filters']))
merged_qs.update(preserved_filters)
if popup:
merged_qs['_popup'] = 1
merged_qs.update(parsed_qs)
parsed_url[4] = urlencode(merged_qs)
return urlunparse(parsed_url)

View File

@ -5,7 +5,16 @@ from django.utils.module_loading import import_by_path
from django.utils.unittest import SkipTest
from django.utils.translation import ugettext as _
class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
available_apps = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
]
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
@classmethod

View File

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

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

View File

@ -17,7 +17,8 @@ from django.utils.module_loading import import_by_path
from django.utils.translation import ugettext_noop as _
UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
HASHERS = None # lazily loaded from PASSWORD_HASHERS
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
@ -30,7 +31,7 @@ def reset_hashers(**kwargs):
def is_password_usable(encoded):
if encoded is None or encoded == UNUSABLE_PASSWORD:
if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
return False
try:
hasher = identify_hasher(encoded)
@ -47,7 +48,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
If setter is specified, it'll be called when you need to
regenerate the password.
"""
if not password or not is_password_usable(encoded):
if not is_password_usable(encoded):
return False
preferred = get_hasher(preferred)
@ -64,13 +65,15 @@ def make_password(password, salt=None, hasher='default'):
"""
Turn a plain-text password into a hash for database storage
Same as encode() but generates a new random salt. If
password is None or blank then UNUSABLE_PASSWORD will be
returned which disallows logins.
Same as encode() but generates a new random salt.
If password is None then a concatenation of
UNUSABLE_PASSWORD_PREFIX and a random string will be returned
which disallows logins. Additional random string reduces chances
of gaining access to staff or superuser accounts.
See ticket #20079 for more info.
"""
if not password:
return UNUSABLE_PASSWORD
if password is None:
return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher)
if not salt:
@ -171,12 +174,12 @@ class BasePasswordHasher(object):
name = mod_path = self.library
try:
module = importlib.import_module(mod_path)
except ImportError:
raise ValueError("Couldn't load %s password algorithm "
"library" % name)
except ImportError as e:
raise ValueError("Couldn't load %r algorithm library: %s" %
(self.__class__.__name__, e))
return module
raise ValueError("Hasher '%s' doesn't specify a library attribute" %
self.__class__)
raise ValueError("Hasher %r doesn't specify a library attribute" %
self.__class__.__name__)
def salt(self):
"""
@ -222,7 +225,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
digest = hashlib.sha256
def encode(self, password, salt, iterations=None):
assert password
assert password is not None
assert salt and '$' not in salt
if not iterations:
iterations = self.iterations
@ -350,7 +353,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
algorithm = "sha1"
def encode(self, password, salt):
assert password
assert password is not None
assert salt and '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
@ -378,7 +381,7 @@ class MD5PasswordHasher(BasePasswordHasher):
algorithm = "md5"
def encode(self, password, salt):
assert password
assert password is not None
assert salt and '$' not in salt
hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)

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

View File

@ -16,7 +16,7 @@ from django.utils import timezone
from django.contrib import auth
# UNUSABLE_PASSWORD is still imported here for backwards compatibility
from django.contrib.auth.hashers import (
check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
check_password, make_password, is_password_usable)
from django.contrib.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import python_2_unicode_compatible

View File

@ -156,6 +156,18 @@ class CustomUserNonUniqueUsername(AbstractBaseUser):
app_label = 'auth'
class CustomUserNonListRequiredFields(AbstractBaseUser):
"A user with a non-list REQUIRED_FIELDS"
username = models.CharField(max_length=30, unique=True)
date_of_birth = models.DateField()
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = 'date_of_birth'
class Meta:
app_label = 'auth'
class CustomUserBadRequiredFields(AbstractBaseUser):
"A user with a non-unique username"
username = models.CharField(max_length=30, unique=True)

View File

@ -8,10 +8,18 @@ from django.test import TransactionTestCase
from django.test.utils import override_settings
# This must be a TransactionTestCase because the WSGI auth handler performs
# its own transaction management.
class ModWsgiHandlerTestCase(TransactionTestCase):
"""
Tests for the mod_wsgi authentication handler
"""
available_apps = [
'django.contrib.auth',
'django.contrib.contenttypes',
]
@skipIfCustomUser
def test_check_password(self):
"""

View File

@ -2,9 +2,10 @@
from __future__ import unicode_literals
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
from django.contrib.auth.hashers import (is_password_usable,
check_password, make_password, PBKDF2PasswordHasher, load_hashers,
PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher,
get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
from django.utils import six
from django.utils import unittest
from django.utils.unittest import skipUnless
@ -31,6 +32,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
# Blank passwords
blank_encoded = make_password('')
self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_pkbdf2(self):
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@ -40,6 +47,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
# Blank passwords
blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_sha1(self):
encoded = make_password('lètmein', 'seasalt', 'sha1')
@ -49,6 +62,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
# Blank passwords
blank_encoded = make_password('', 'seasalt', 'sha1')
self.assertTrue(blank_encoded.startswith('sha1$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_md5(self):
encoded = make_password('lètmein', 'seasalt', 'md5')
@ -58,6 +77,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
# Blank passwords
blank_encoded = make_password('', 'seasalt', 'md5')
self.assertTrue(blank_encoded.startswith('md5$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_md5(self):
encoded = make_password('lètmein', '', 'unsalted_md5')
@ -71,6 +96,11 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(is_password_usable(alt_encoded))
self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', alt_encoded))
# Blank passwords
blank_encoded = make_password('', '', 'unsalted_md5')
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1')
@ -82,6 +112,12 @@ class TestUtilsHashPass(unittest.TestCase):
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', alt_encoded))
# Blank passwords
blank_encoded = make_password('', '', 'unsalted_sha1')
self.assertTrue(blank_encoded.startswith('sha1$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
@ -91,6 +127,12 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmei', encoded))
self.assertFalse(check_password('lètmeiz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
# Blank passwords
blank_encoded = make_password('', 'ab', 'crypt')
self.assertTrue(blank_encoded.startswith('crypt$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt_sha256(self):
@ -107,6 +149,12 @@ class TestUtilsHashPass(unittest.TestCase):
encoded = make_password(password, hasher='bcrypt_sha256')
self.assertTrue(check_password(password, encoded))
self.assertFalse(check_password(password[:72], encoded))
# Blank passwords
blank_encoded = make_password('', hasher='bcrypt_sha256')
self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt(self):
@ -116,21 +164,31 @@ class TestUtilsHashPass(unittest.TestCase):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
# Blank passwords
blank_encoded = make_password('', hasher='bcrypt')
self.assertTrue(blank_encoded.startswith('bcrypt$'))
self.assertTrue(is_password_usable(blank_encoded))
self.assertTrue(check_password('', blank_encoded))
self.assertFalse(check_password(' ', blank_encoded))
def test_unusable(self):
encoded = make_password(None)
self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
self.assertFalse(is_password_usable(encoded))
self.assertFalse(check_password(None, encoded))
self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded))
self.assertFalse(check_password(encoded, encoded))
self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
self.assertFalse(check_password('', encoded))
self.assertFalse(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertRaises(ValueError, identify_hasher, encoded)
# Assert that the unusable passwords actually contain a random part.
# This might fail one day due to a hash collision.
self.assertNotEqual(encoded, make_password(None), "Random password collision?")
def test_bad_algorithm(self):
def doit():
with self.assertRaises(ValueError):
make_password('lètmein', hasher='lolcat')
self.assertRaises(ValueError, doit)
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
def test_bad_encoded(self):
@ -178,3 +236,17 @@ class TestUtilsHashPass(unittest.TestCase):
state['upgraded'] = True
self.assertFalse(check_password('WRONG', encoded, setter))
self.assertFalse(state['upgraded'])
def test_load_library_no_algorithm(self):
with self.assertRaises(ValueError) as e:
BasePasswordHasher()._load_library()
self.assertEqual("Hasher 'BasePasswordHasher' doesn't specify a "
"library attribute", str(e.exception))
def test_load_library_importerror(self):
PlainHasher = type(str('PlainHasher'), (BasePasswordHasher,),
{'algorithm': 'plain', 'library': 'plain'})
# Python 3.3 adds quotes around module name
with six.assertRaisesRegex(self, ValueError,
"Couldn't load 'PlainHasher' algorithm library: No module named '?plain'?"):
PlainHasher()._load_library()

View File

@ -174,6 +174,13 @@ class CreatesuperuserManagementCommandTestCase(TestCase):
class CustomUserModelValidationTestCase(TestCase):
@override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
def test_required_fields_is_list(self):
"REQUIRED_FIELDS should be a list."
new_io = StringIO()
get_validation_errors(new_io, get_app('auth'))
self.assertIn("The REQUIRED_FIELDS must be a list or tuple.", new_io.getvalue())
@override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields')
def test_username_not_in_required_fields(self):
"USERNAME_FIELD should not appear in REQUIRED_FIELDS."

View File

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

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router
from django.db.models import get_apps, get_models, signals
from django.db.models import get_apps, get_model, get_models, signals, UnavailableApp
from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
@ -11,6 +11,11 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
Creates content types for models in the given app, removing any model
entries that no longer have a matching model class.
"""
try:
get_model('contenttypes', 'ContentType')
except UnavailableApp:
return
if not router.allow_syncdb(db, ContentType):
return

View File

@ -17,11 +17,17 @@ class FlatpageForm(forms.ModelForm):
def clean_url(self):
url = self.cleaned_data['url']
if not url.startswith('/'):
raise forms.ValidationError(ugettext("URL is missing a leading slash."))
raise forms.ValidationError(
ugettext("URL is missing a leading slash."),
code='missing_leading_slash',
)
if (settings.APPEND_SLASH and
'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and
not url.endswith('/')):
raise forms.ValidationError(ugettext("URL is missing a trailing slash."))
raise forms.ValidationError(
ugettext("URL is missing a trailing slash."),
code='missing_trailing_slash',
)
return url
def clean(self):
@ -36,7 +42,9 @@ class FlatpageForm(forms.ModelForm):
for site in sites:
if same_url.filter(sites=site).exists():
raise forms.ValidationError(
_('Flatpage with url %(url)s already exists for site %(site)s') %
{'url': url, 'site': site})
_('Flatpage with url %(url)s already exists for site %(site)s'),
code='duplicate_url',
params={'url': url, 'site': site},
)
return super(FlatpageForm, self).clean()

View File

@ -7,6 +7,7 @@ from django.forms import formsets, ValidationError
from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext as _
from django.utils import six
from django.contrib.formtools.wizard.storage import get_storage
@ -271,7 +272,9 @@ class WizardView(TemplateView):
management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid():
raise ValidationError(
'ManagementForm data is missing or has been tampered.')
_('ManagementForm data is missing or has been tampered.'),
code='missing_management_form',
)
form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and

View File

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

View File

@ -22,7 +22,9 @@ def _simple_domain_name_validator(value):
checks = ((s in value) for s in string.whitespace)
if any(checks):
raise ValidationError(
_("The domain name cannot contain any spaces or tabs."))
_("The domain name cannot contain any spaces or tabs."),
code='invalid',
)
class SiteManager(models.Manager):

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
from functools import reduce
import operator
from django.utils.encoding import force_text
class DjangoRuntimeWarning(RuntimeWarning):
@ -74,46 +77,65 @@ NON_FIELD_ERRORS = '__all__'
class ValidationError(Exception):
"""An error while validating data."""
def __init__(self, message, code=None, params=None):
import operator
from django.utils.encoding import force_text
"""
ValidationError can be passed any object that can be printed (usually
a string), a list of objects or a dictionary.
"""
if isinstance(message, dict):
self.message_dict = message
# Reduce each list of messages into a single list.
message = reduce(operator.add, message.values())
if isinstance(message, list):
self.messages = [force_text(msg) for msg in message]
self.error_dict = message
elif isinstance(message, list):
self.error_list = message
else:
self.code = code
self.params = params
self.message = message
self.error_list = [self]
@property
def message_dict(self):
message_dict = {}
for field, messages in self.error_dict.items():
message_dict[field] = []
for message in messages:
if isinstance(message, ValidationError):
message_dict[field].extend(message.messages)
else:
message_dict[field].append(force_text(message))
return message_dict
@property
def messages(self):
if hasattr(self, 'error_dict'):
message_list = reduce(operator.add, self.error_dict.values())
else:
message_list = self.error_list
messages = []
for message in message_list:
if isinstance(message, ValidationError):
params = message.params
message = message.message
if params:
message %= params
message = force_text(message)
self.messages = [message]
messages.append(message)
return messages
def __str__(self):
# This is needed because, without a __str__(), printing an exception
# instance would result in this:
# AttributeError: ValidationError instance has no attribute 'args'
# See http://www.python.org/doc/current/tut/node10.html#handling
if hasattr(self, 'message_dict'):
if hasattr(self, 'error_dict'):
return repr(self.message_dict)
return repr(self.messages)
def __repr__(self):
if hasattr(self, 'message_dict'):
return 'ValidationError(%s)' % repr(self.message_dict)
return 'ValidationError(%s)' % repr(self.messages)
return 'ValidationError(%s)' % self
def update_error_dict(self, error_dict):
if hasattr(self, 'message_dict'):
if hasattr(self, 'error_dict'):
if error_dict:
for k, v in self.message_dict.items():
for k, v in self.error_dict.items():
error_dict.setdefault(k, []).extend(v)
else:
error_dict = self.message_dict
error_dict = self.error_dict
else:
error_dict[NON_FIELD_ERRORS] = self.messages
error_dict[NON_FIELD_ERRORS] = self.error_list
return error_dict

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]
verbosity = int(options.get('verbosity'))
interactive = options.get('interactive')
# 'reset_sequences' is a stealth option
# The following are stealth options used by Django's internals.
reset_sequences = options.get('reset_sequences', True)
allow_cascade = options.get('allow_cascade', False)
inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
self.style = no_style()
@ -45,7 +47,9 @@ class Command(NoArgsCommand):
except ImportError:
pass
sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)
sql_list = sql_flush(self.style, connection, only_django=True,
reset_sequences=reset_sequences,
allow_cascade=allow_cascade)
if interactive:
confirm = input("""You have requested a flush of the database.
@ -72,16 +76,9 @@ Are you sure you want to do this?
"Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.\n"
"The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
# Emit the post sync signal. This allows individual
# applications to respond as if the database had been
# sync'd from scratch.
all_models = []
for app in models.get_apps():
all_models.extend([
m for m in models.get_models(app, include_auto_created=True)
if router.allow_syncdb(db, m)
])
emit_post_sync_signal(set(all_models), verbosity, interactive, db)
if not inhibit_post_syncdb:
self.emit_post_syncdb(verbosity, interactive, db)
# Reinstall the initial_data fixture.
if options.get('load_initial_data'):
@ -90,3 +87,15 @@ Are you sure you want to do this?
else:
self.stdout.write("Flush cancelled.\n")
@staticmethod
def emit_post_syncdb(verbosity, interactive, database):
# Emit the post sync signal. This allows individual applications to
# respond as if the database had been sync'd from scratch.
all_models = []
for app in models.get_apps():
all_models.extend([
m for m in models.get_models(app, include_auto_created=True)
if router.allow_syncdb(database, m)
])
emit_post_sync_signal(set(all_models), verbosity, interactive, database)

View File

@ -40,6 +40,11 @@ class Command(BaseCommand):
return get_internal_wsgi_application()
def handle(self, addrport='', *args, **options):
from django.conf import settings
if not settings.DEBUG and not settings.ALLOWED_HOSTS:
raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')
self.use_ipv6 = options.get('use_ipv6')
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')

View File

@ -102,7 +102,7 @@ def sql_delete(app, style, connection):
return output[::-1] # Reverse it, to deal with table dependencies.
def sql_flush(style, connection, only_django=False, reset_sequences=True):
def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_cascade=False):
"""
Returns a list of the SQL statements used to flush the database.
@ -114,7 +114,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True):
else:
tables = connection.introspection.table_names()
seqs = connection.introspection.sequence_list() if reset_sequences else ()
statements = connection.ops.sql_flush(style, tables, seqs)
statements = connection.ops.sql_flush(style, tables, seqs, allow_cascade)
return statements

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 settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name):
# Check that REQUIRED_FIELDS is a list
if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
e.add(opts, 'The REQUIRED_FIELDS must be a list or tuple.')
# Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')

View File

@ -422,8 +422,11 @@ class RegexURLResolver(LocaleRegexProvider):
lookup_view_s = "%s.%s" % (m, n)
else:
lookup_view_s = lookup_view
patterns = [pattern for (possibility, pattern, defaults) in possibilities]
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"arguments '%s' not found." % (lookup_view_s, args, kwargs))
"arguments '%s' not found. %d pattern(s) tried: %s" %
(lookup_view_s, args, kwargs, len(patterns), patterns))
class LocaleRegexURLResolver(RegexURLResolver):
"""

View File

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

View File

@ -390,7 +390,7 @@ class BaseDatabaseWrapper(object):
def disable_constraint_checking(self):
"""
Backends can implement as needed to temporarily disable foreign key
constraint checking. Should return True if the constraints were
constraint checking. Should return True if the constraints were
disabled and will need to be reenabled.
"""
return False
@ -966,7 +966,7 @@ class BaseDatabaseOperations(object):
"""
return ''
def sql_flush(self, style, tables, sequences):
def sql_flush(self, style, tables, sequences, allow_cascade=False):
"""
Returns a list of SQL statements required to remove all data from
the given database tables (without actually removing the tables
@ -977,6 +977,10 @@ class BaseDatabaseOperations(object):
The `style` argument is a Style object as returned by either
color_style() or no_style() in django.core.management.color.
The `allow_cascade` argument determines whether truncation may cascade
to tables with foreign keys pointing the tables being truncated.
PostgreSQL requires a cascade even if these tables are empty.
"""
raise NotImplementedError()

View File

@ -302,14 +302,17 @@ class DatabaseOperations(BaseDatabaseOperations):
def random_function_sql(self):
return 'RAND()'
def sql_flush(self, style, tables, sequences):
def sql_flush(self, style, tables, sequences, allow_cascade=False):
# NB: The generated SQL below is specific to MySQL
# 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
# to clear all tables of all data
if tables:
sql = ['SET FOREIGN_KEY_CHECKS = 0;']
for table in tables:
sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table))))
sql.append('%s %s;' % (
style.SQL_KEYWORD('TRUNCATE'),
style.SQL_FIELD(self.quote_name(table)),
))
sql.append('SET FOREIGN_KEY_CHECKS = 1;')
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
from functools import wraps
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models
from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp
from django.db.models.query import Q
from django.db.models.expressions import F
from django.db.models.manager import Manager

View File

@ -450,16 +450,18 @@ class Model(six.with_metaclass(ModelBase)):
need to do things manually, as they're dynamically created classes and
only module-level classes can be pickled by the default path.
"""
if not self._deferred:
return super(Model, self).__reduce__()
data = self.__dict__
if not self._deferred:
class_id = self._meta.app_label, self._meta.object_name
return model_unpickle, (class_id, [], simple_class_factory), data
defers = []
for field in self._meta.fields:
if isinstance(self.__class__.__dict__.get(field.attname),
DeferredAttribute):
DeferredAttribute):
defers.append(field.attname)
model = self._meta.proxy_for_model
return (model_unpickle, (model, defers), data)
class_id = model._meta.app_label, model._meta.object_name
return (model_unpickle, (class_id, defers, deferred_class_factory), data)
def _get_pk_val(self, meta=None):
if not meta:
@ -907,7 +909,7 @@ class Model(six.with_metaclass(ModelBase)):
'field_label': six.text_type(field_labels)
}
def full_clean(self, exclude=None):
def full_clean(self, exclude=None, validate_unique=True):
"""
Calls clean_fields, clean, and validate_unique, on the model,
and raises a ``ValidationError`` for any errors that occurred.
@ -929,13 +931,14 @@ class Model(six.with_metaclass(ModelBase)):
errors = e.update_error_dict(errors)
# Run unique checks, but only for fields that passed validation.
for name in errors.keys():
if name != NON_FIELD_ERRORS and name not in exclude:
exclude.append(name)
try:
self.validate_unique(exclude=exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
if validate_unique:
for name in errors.keys():
if name != NON_FIELD_ERRORS and name not in exclude:
exclude.append(name)
try:
self.validate_unique(exclude=exclude)
except ValidationError as e:
errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)
@ -960,7 +963,7 @@ class Model(six.with_metaclass(ModelBase)):
try:
setattr(self, f.attname, f.clean(raw_value, self))
except ValidationError as e:
errors[f.name] = e.messages
errors[f.name] = e.error_list
if errors:
raise ValidationError(errors)
@ -1007,12 +1010,22 @@ def get_absolute_url(opts, func, self, *args, **kwargs):
class Empty(object):
pass
def simple_class_factory(model, attrs):
"""
Needed for dynamic classes.
"""
return model
def model_unpickle(model, attrs):
def model_unpickle(model_id, attrs, factory):
"""
Used to unpickle Model subclasses with deferred fields.
"""
cls = deferred_class_factory(model, attrs)
if isinstance(model_id, tuple):
model = get_model(*model_id)
else:
# Backwards compat - the model was cached directly in earlier versions.
model = model_id
cls = factory(model, attrs)
return cls.__new__(cls)
model_unpickle.__safe_for_unpickle__ = True

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -170,11 +170,6 @@ class BaseForm(object):
if bf.label:
label = conditional_escape(force_text(bf.label))
# Only add the suffix if the label does not end in
# punctuation.
if self.label_suffix:
if label[-1] not in ':?.!':
label = format_html('{0}{1}', label, self.label_suffix)
label = bf.label_tag(label) or ''
else:
label = ''
@ -522,6 +517,9 @@ class BoundField(object):
If attrs are given, they're used as HTML attributes on the <label> tag.
"""
contents = contents or self.label
# Only add the suffix if the label does not end in punctuation.
if self.form.label_suffix and contents and contents[-1] not in ':?.!':
contents = format_html('{0}{1}', contents, self.form.label_suffix)
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
if id_:

View File

@ -85,7 +85,10 @@ class BaseFormSet(object):
if self.is_bound:
form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
if not form.is_valid():
raise ValidationError('ManagementForm data is missing or has been tampered with')
raise ValidationError(
_('ManagementForm data is missing or has been tampered with'),
code='missing_management_form',
)
else:
form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
TOTAL_FORM_COUNT: self.total_form_count(),
@ -263,6 +266,13 @@ class BaseFormSet(object):
self.full_clean()
return self._errors
def total_error_count(self):
"""
Returns the number of errors across all forms in the formset.
"""
return len(self.non_form_errors()) +\
sum(len(form_errors) for form_errors in self.errors)
def _should_delete_form(self, form):
"""
Returns whether or not the form was marked for deletion.
@ -308,7 +318,9 @@ class BaseFormSet(object):
self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max:
raise ValidationError(ungettext(
"Please submit %d or fewer forms.",
"Please submit %d or fewer forms.", self.max_num) % self.max_num)
"Please submit %d or fewer forms.", self.max_num) % self.max_num,
code='too_many_forms',
)
# Give self.clean() a chance to do cross-form validation.
self.clean()
except ValidationError as e:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,11 +6,12 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
LANGUAGE =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
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
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

View File

@ -77,6 +77,9 @@ django_next_version = '1.6'
# for a list of supported languages.
#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
# non-false value, then it is used:
#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
.. 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
---------------------------

View File

@ -34,6 +34,15 @@ command. For example:
.. _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
-----------

View File

@ -526,6 +526,17 @@ Marc Tamlyn
.. _CCBV: http://ccbv.co.uk/
.. _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
===================

View File

@ -37,7 +37,7 @@ and time availability), claim it by following these steps:
`ticket tracker`_.
* 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.
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
@ -48,7 +48,7 @@ and time availability), claim it by following these steps:
* 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,
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
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:
* If :setting:`DEBUG` is set to ``True`` (in your settings module) then your

View File

@ -192,22 +192,24 @@ RedirectView
permanent = False
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.update_counter()
return reverse('product_detail', args=(pk,))
return super(ArticleCounterRedirectView, self).get_redirect_url(*args, **kwargs)
**Example urls.py**::
from django.conf.urls import patterns, url
from django.views.generic.base import RedirectView
from article.views import ArticleCounterRedirectView
from article.views import ArticleCounterRedirectView, ArticleDetail
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'),
)
@ -218,6 +220,11 @@ RedirectView
The URL to redirect to, as a string. Or ``None`` to raise a 410 (Gone)
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
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
to all subclass views.
**Example views.py**::
**Example myapp/views.py**::
from django.conf.urls import patterns, url
from django.views.generic.dates import ArchiveIndexView
@ -160,7 +160,7 @@ YearArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_year``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.dates import YearArchiveView
@ -172,7 +172,7 @@ YearArchiveView
make_object_list = True
allow_future = True
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -255,7 +255,7 @@ MonthArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_month``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.dates import MonthArchiveView
@ -267,7 +267,7 @@ MonthArchiveView
make_object_list = True
allow_future = True
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -348,7 +348,7 @@ WeekArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_week``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.dates import WeekArchiveView
@ -361,7 +361,7 @@ WeekArchiveView
week_format = "%W"
allow_future = True
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -463,7 +463,7 @@ DayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_day``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.dates import DayArchiveView
@ -475,7 +475,7 @@ DayArchiveView
make_object_list = True
allow_future = True
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -537,7 +537,7 @@ TodayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_today``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.dates import TodayArchiveView
@ -549,7 +549,7 @@ TodayArchiveView
make_object_list = True
allow_future = True
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -599,7 +599,7 @@ DateDetailView
* Uses a default ``template_name_suffix`` of ``_detail``.
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
from django.views.generic.dates import DateDetailView

View File

@ -36,7 +36,7 @@ DetailView
9. ``get()``
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.utils import timezone
@ -52,7 +52,7 @@ DetailView
context['now'] = timezone.now()
return context
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -62,6 +62,16 @@ DetailView
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
--------
@ -111,7 +121,7 @@ ListView
context['now'] = timezone.now()
return context
**Example urls.py**::
**Example myapp/urls.py**::
from django.conf.urls import patterns, url
@ -121,6 +131,19 @@ ListView
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
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.base.View`
**Example forms.py**::
**Example myapp/forms.py**::
from django import forms
@ -54,7 +54,7 @@ FormView
# send email using the self.cleaned_data dictionary
pass
**Example views.py**::
**Example myapp/views.py**::
from myapp.forms import ContactForm
from django.views.generic.edit import FormView
@ -70,6 +70,16 @@ FormView
form.send_email()
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
----------
@ -101,7 +111,7 @@ CreateView
creating objects for the example ``Author`` model would cause the
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 myapp.models import Author
@ -110,6 +120,15 @@ CreateView
model = Author
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
----------
@ -143,7 +162,7 @@ UpdateView
updating objects for the example ``Author`` model would cause the
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 myapp.models import Author
@ -151,6 +170,16 @@ UpdateView
class AuthorUpdate(UpdateView):
model = Author
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
----------
@ -184,8 +213,7 @@ DeleteView
deleting objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_check_delete.html'``.
**Example views.py**::
**Example myapp/views.py**::
from django.views.generic.edit import DeleteView
from django.core.urlresolvers import reverse_lazy
@ -194,3 +222,12 @@ DeleteView
class AuthorDelete(DeleteView):
model = Author
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``,
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
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
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'),
(r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'),
(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete'),
url(r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'),
url(r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm', name='password_reset_confirm'),
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
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
: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)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
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()
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::
urlpatterns = patterns('',
('^pages/', include('django.contrib.flatpages.urls')),
(r'^pages/', include('django.contrib.flatpages.urls')),
)
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::
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
@ -82,9 +82,15 @@ to place the pattern at the end of the other urlpatterns::
# Your other patterns here
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
to hard code the urls, so you can reference them with the :ttag:`url` template
tag::

View File

@ -166,7 +166,7 @@ This template expects a ``wizard`` object that has various items attached to
it:
* ``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).
* ``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
==================
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
-------

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'``.
.. attribute:: Form.label_suffix
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
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)
>>> 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
auto-generated label tag. An optional ``attrs`` dictionary may contain
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()
When you use Django's rendering shortcuts, CSS classes are used to
@ -688,6 +695,29 @@ by a ``Widget``::
>>> print(bound_form['subject'].value())
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 to a form

View File

@ -5,7 +5,7 @@ Model Form Functions
.. module:: django.forms.models
: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``.
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.
``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
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.
.. versionchanged:: 1.6
@ -35,14 +42,16 @@ Model Form Functions
information. Omitting any definition of the fields to use will result in all
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.
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`.
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@ -54,9 +63,10 @@ Model Form Functions
.. 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
defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@ -69,4 +79,5 @@ Model Form Functions
.. 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.
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
the ``ValidationError`` constructor. If no ``ValidationError`` is raised, the
method should return the cleaned (normalized) data as a Python object.
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.
problem with the data it is processing, passing the relevant information to
the ``ValidationError`` constructor. :ref:`See below <raising-validation-error>`
for the best practice in raising ``ValidationError``. If no ``ValidationError``
is raised, the method should return the cleaned (normalized) data as a Python
object.
Most validation can be done using `validators`_ - simple helpers that can be
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
``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`` 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
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
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
------------------------------------------

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>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
@ -251,7 +254,7 @@ foundation for custom widgets.
into two separate values::
from django.forms import 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
: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
:meth:`Model.validate_unique()`, in that order and raises a
:exc:`~django.core.exceptions.ValidationError` that has a ``message_dict``
attribute containing errors from all three stages.
:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``, in that
order and raises a :exc:`~django.core.exceptions.ValidationError` that has a
``message_dict`` attribute containing errors from all three stages.
The optional ``exclude`` argument can be used to provide a list of field names
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
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
.. setting:: DEBUG_PROPAGATE_EXCEPTIONS

View File

@ -498,17 +498,20 @@ request_finished
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
Before Django 1.5, this signal was fired before sending the content to the
client. In order to accomodate streaming responses, it is now fired after
sending the content.
Before Django 1.5, this signal was sent before delivering content to the
client. In order to accommodate :ref:`streaming responses
<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:

View File

@ -371,7 +371,7 @@ displayed if the given array is empty or could not be found::
{% for athlete in athlete_list %}
<li>{{ athlete.name }}</li>
{% empty %}
<li>Sorry, no athlete in this list!</li>
<li>Sorry, no athletes in this list.</li>
{% endfor %}
<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" />
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

View File

@ -659,6 +659,8 @@ Functions for working with Python modules.
.. function:: import_by_path(dotted_path, error_prefix='')
.. versionadded:: 1.6
Imports a dotted module path and returns the attribute/class designated by
the last name in the path. Raises
: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
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
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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
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
~~~~~~~~~~~~~~
@ -236,9 +243,14 @@ Minor features
.. _`Pillow`: https://pypi.python.org/pypi/Pillow
.. _`PIL`: https://pypi.python.org/pypi/PIL
* :doc:`ModelForm </topics/forms/modelforms/>` accepts a new
Meta option: ``localized_fields``. Fields included in this list will be localized
(by setting ``localize`` on the form field).
* :class:`~django.forms.ModelForm` accepts several new ``Meta``
options.
* 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
instead of requiring an iterable of lists or tuples.
@ -303,6 +315,21 @@ Minor features
:class:`~django.contrib.admin.InlineModelAdmin` may be overridden to
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
=====================================
@ -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()`
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
~~~~~~~~~~~~~
@ -634,6 +702,29 @@ Miscellaneous
:meth:`django.contrib.auth.logout` which will send the
: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
==========================

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
systems.
See the `authentication backend reference
See the :ref:`authentication backend reference
<authentication-backends-reference>` for information on the authentication
backends included with Django.
@ -583,12 +583,28 @@ The following methods are available on any subclass of
password hashing. Doesn't save the
: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)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
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()
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``
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])
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 books.models import Author
class RecordInterest(View, SingleObjectMixin):
class RecordInterest(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
@ -446,7 +446,7 @@ Our new ``AuthorDetail`` looks like this::
class AuthorInterestForm(forms.Form):
message = forms.CharField()
class AuthorDetail(DetailView, FormMixin):
class AuthorDetail(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
@ -553,7 +553,7 @@ template as ``AuthorDisplay`` is using on ``GET``.
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
class AuthorInterest(FormView, SingleObjectMixin):
class AuthorInterest(SingleObjectMixin, FormView):
template_name = 'books/author_detail.html'
form_class = AuthorInterestForm
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
>>> with open('/tmp/hello.world', 'w') as f:
>>> myfile = File(f)
>>> for line in myfile:
>>> print line
... myfile = File(f)
... myfile.write('Hello World')
...
>>> myfile.closed
True
>>> f.closed

View File

@ -3,7 +3,7 @@
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
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
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
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
... return
... titles = []
... for i in range(0, self.total_form_count()):
... form = self.forms[i]
... for form in self.forms:
... title = form.cleaned_data['title']
... if title in titles:
... raise forms.ValidationError("Articles in a set must have distinct titles.")

View File

@ -302,7 +302,7 @@ loop::
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }}: {{ field }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
<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``.
``{{ field.label_tag }}``
The field's label wrapped in the appropriate HTML ``<label>`` tag,
e.g. ``<label for="id_email">Email address</label>``
The field's label wrapped in the appropriate HTML ``<label>`` tag.
.. 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 }}``
The value of the field. e.g ``someone@example.com``
@ -375,7 +381,7 @@ these two methods::
{% for field in form.visible_fields %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }}: {{ field }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
<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 %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }}: {{ field }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}

View File

@ -49,7 +49,7 @@ define the media requirements.
Here's a simple example::
from django import froms
from django import forms
class CalendarWidget(forms.TextInput):
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).
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
--------------
@ -388,8 +388,10 @@ include that field.
.. _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
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.,
``Textarea(...)``) or classes (e.g., ``Textarea``).
If you want to further customize a field -- including its type, label, 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.
.. versionadded:: 1.6
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::
from django.forms import ModelForm
from myapp.models import Article
class ArticleForm(ModelForm):
pub_date = MyDateFormField()
slug = MySlugFormField()
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter']
If you want to override a field's default label, then specify the ``label``
parameter when declaring the form field::
If you want to override a field's default validators, then specify the
``validators`` parameter when declaring the form field::
from django.forms import ModelForm, DateField
from myapp.models import Article
class ArticleForm(ModelForm):
pub_date = DateField(label='Publication date')
slug = CharField(validators=[validate_slug])
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter']
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
.. note::
@ -597,7 +626,7 @@ example by specifying the widgets to be used for a given field::
>>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm,
widgets={"title": Textarea()})
... widgets={"title": Textarea()})
The fields to include can be specified using the ``fields`` and ``exclude``
keyword arguments, or the corresponding attributes on the ``ModelForm`` inner
@ -907,7 +936,7 @@ Third, you can manually render each field::
{{ formset.management_form }}
{% for form in formset %}
{% for field in form %}
{{ field.label_tag }}: {{ field }}
{{ field.label_tag }} {{ field }}
{% endfor %}
{% endfor %}
</form>

View File

@ -17,6 +17,11 @@ Here are a couple of example settings::
DEFAULT_FROM_EMAIL = 'webmaster@example.com'
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:
* 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``
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
=====================================

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