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