Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/db/models/loading.py
This commit is contained in:
commit
9daf81b94e
|
@ -4,5 +4,6 @@
|
|||
MANIFEST
|
||||
dist/
|
||||
docs/_build/
|
||||
docs/locale/
|
||||
tests/coverage_html/
|
||||
tests/.coverage
|
||||
|
|
3
AUTHORS
3
AUTHORS
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
{% load admin_urls %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{% extends "admin/base_site.html" %}
|
||||
{% load i18n %}
|
||||
{% load admin_urls %}
|
||||
{% load i18n admin_urls %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 "
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) .
|
||||
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
-----------
|
||||
|
||||
|
|
|
@ -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
|
||||
===================
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
-------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
------------------------------------------
|
||||
|
|
|
@ -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):
|
||||
|
||||
# ...
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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
|
||||
==========================
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue