summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--AUTHORS3
-rw-r--r--django/contrib/admin/forms.py9
-rw-r--r--django/contrib/admin/options.py117
-rw-r--r--django/contrib/admin/templates/admin/change_form.html8
-rw-r--r--django/contrib/admin/templates/admin/change_list.html8
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html3
-rw-r--r--django/contrib/admin/templates/admin/delete_selected_confirmation.html3
-rw-r--r--django/contrib/admin/templates/admin/object_history.html3
-rw-r--r--django/contrib/admin/templates/admin/submit_line.html5
-rw-r--r--django/contrib/admin/templatetags/admin_list.py2
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py3
-rw-r--r--django/contrib/admin/templatetags/admin_urls.py42
-rw-r--r--django/contrib/admin/tests.py9
-rw-r--r--django/contrib/admin/views/main.py1
-rw-r--r--django/contrib/auth/forms.py39
-rw-r--r--django/contrib/auth/hashers.py37
-rw-r--r--django/contrib/auth/management/__init__.py15
-rw-r--r--django/contrib/auth/models.py2
-rw-r--r--django/contrib/auth/tests/test_custom_user.py12
-rw-r--r--django/contrib/auth/tests/test_handlers.py8
-rw-r--r--django/contrib/auth/tests/test_hashers.py84
-rw-r--r--django/contrib/auth/tests/test_management.py7
-rw-r--r--django/contrib/auth/tests/test_models.py2
-rw-r--r--django/contrib/contenttypes/management.py7
-rw-r--r--django/contrib/flatpages/forms.py16
-rw-r--r--django/contrib/formtools/wizard/views.py5
-rw-r--r--django/contrib/gis/forms/fields.py6
-rw-r--r--django/contrib/sites/models.py4
-rw-r--r--django/core/compat_checks/__init__.py (renamed from tests/shared_models/__init__.py)0
-rw-r--r--django/core/compat_checks/base.py39
-rw-r--r--django/core/compat_checks/django_1_6_0.py37
-rw-r--r--django/core/exceptions.py64
-rw-r--r--django/core/management/commands/checksetup.py14
-rw-r--r--django/core/management/commands/flush.py33
-rw-r--r--django/core/management/commands/runserver.py5
-rw-r--r--django/core/management/sql.py4
-rw-r--r--django/core/management/validation.py4
-rw-r--r--django/core/urlresolvers.py5
-rw-r--r--django/core/validators.py8
-rw-r--r--django/db/backends/__init__.py8
-rw-r--r--django/db/backends/mysql/base.py7
-rw-r--r--django/db/backends/oracle/base.py12
-rw-r--r--django/db/backends/postgresql_psycopg2/operations.py19
-rw-r--r--django/db/backends/sqlite3/base.py12
-rw-r--r--django/db/models/__init__.py2
-rw-r--r--django/db/models/base.py43
-rw-r--r--django/db/models/fields/__init__.py142
-rw-r--r--django/db/models/fields/related.py7
-rw-r--r--django/db/models/loading.py75
-rw-r--r--django/db/models/sql/compiler.py64
-rw-r--r--django/db/models/sql/query.py29
-rw-r--r--django/forms/fields.py111
-rw-r--r--django/forms/forms.py8
-rw-r--r--django/forms/formsets.py16
-rw-r--r--django/forms/models.py102
-rw-r--r--django/forms/util.py13
-rw-r--r--django/http/response.py4
-rw-r--r--django/test/client.py2
-rw-r--r--django/test/testcases.py73
-rw-r--r--django/utils/ipv6.py5
-rw-r--r--django/views/generic/base.py20
-rw-r--r--django/views/generic/edit.py4
-rw-r--r--docs/Makefile3
-rw-r--r--docs/conf.py3
-rw-r--r--docs/howto/deployment/wsgi/index.txt8
-rw-r--r--docs/howto/deployment/wsgi/uwsgi.txt9
-rw-r--r--docs/internals/committers.txt11
-rw-r--r--docs/internals/contributing/writing-code/submitting-patches.txt4
-rw-r--r--docs/intro/tutorial03.txt7
-rw-r--r--docs/ref/class-based-views/base.txt15
-rw-r--r--docs/ref/class-based-views/generic-date-based.txt24
-rw-r--r--docs/ref/class-based-views/generic-display.txt29
-rw-r--r--docs/ref/class-based-views/generic-editing.txt49
-rw-r--r--docs/ref/contrib/admin/index.txt14
-rw-r--r--docs/ref/contrib/auth.txt16
-rw-r--r--docs/ref/contrib/flatpages.txt12
-rw-r--r--docs/ref/contrib/formtools/form-wizard.txt2
-rw-r--r--docs/ref/django-admin.txt13
-rw-r--r--docs/ref/forms/api.txt32
-rw-r--r--docs/ref/forms/models.txt29
-rw-r--r--docs/ref/forms/validation.txt110
-rw-r--r--docs/ref/forms/widgets.txt5
-rw-r--r--docs/ref/models/instances.txt14
-rw-r--r--docs/ref/settings.txt4
-rw-r--r--docs/ref/signals.txt19
-rw-r--r--docs/ref/templates/builtins.txt4
-rw-r--r--docs/ref/utils.txt2
-rw-r--r--docs/releases/1.5.txt10
-rw-r--r--docs/releases/1.6.txt97
-rw-r--r--docs/topics/auth/customizing.txt18
-rw-r--r--docs/topics/auth/passwords.txt6
-rw-r--r--docs/topics/class-based-views/mixins.txt6
-rw-r--r--docs/topics/files.txt6
-rw-r--r--docs/topics/forms/formsets.txt22
-rw-r--r--docs/topics/forms/index.txt16
-rw-r--r--docs/topics/forms/media.txt2
-rw-r--r--docs/topics/forms/modelforms.txt61
-rw-r--r--docs/topics/settings.txt5
-rw-r--r--docs/topics/testing/advanced.txt74
-rw-r--r--docs/topics/testing/overview.txt52
-rw-r--r--setup.py2
-rw-r--r--tests/admin_inlines/tests.py4
-rw-r--r--tests/admin_scripts/tests.py22
-rw-r--r--tests/admin_util/tests.py4
-rw-r--r--tests/admin_views/tests.py167
-rw-r--r--tests/admin_widgets/tests.py8
-rw-r--r--tests/backends/tests.py7
-rw-r--r--tests/basic/tests.py3
-rw-r--r--tests/cache/tests.py2
-rw-r--r--tests/compat_checks/__init__.py0
-rw-r--r--tests/compat_checks/models.py1
-rw-r--r--tests/compat_checks/tests.py107
-rw-r--r--tests/delete_regress/tests.py6
-rw-r--r--tests/file_storage/tests.py20
-rw-r--r--tests/file_storage/urls.py7
-rw-r--r--tests/fixtures/tests.py7
-rw-r--r--tests/fixtures_model_package/tests.py3
-rw-r--r--tests/fixtures_regress/tests.py6
-rw-r--r--tests/forms_tests/tests/test_forms.py43
-rw-r--r--tests/forms_tests/tests/test_formsets.py154
-rw-r--r--tests/forms_tests/tests/test_regressions.py4
-rw-r--r--tests/forms_tests/tests/test_widgets.py2
-rw-r--r--tests/forms_tests/tests/tests.py6
-rw-r--r--tests/generic_views/test_base.py20
-rw-r--r--tests/get_or_create/tests.py2
-rw-r--r--tests/handlers/tests.py2
-rw-r--r--tests/lookup/models.py21
-rw-r--r--tests/lookup/tests.py793
-rw-r--r--tests/m2m_through_regress/tests.py1
-rw-r--r--tests/middleware/tests.py3
-rw-r--r--tests/model_forms/models.py37
-rw-r--r--tests/model_forms/tests.py179
-rw-r--r--tests/model_forms_regress/tests.py4
-rw-r--r--tests/model_formsets/tests.py52
-rw-r--r--tests/multiple_database/tests.py6
-rw-r--r--tests/proxy_model_inheritance/tests.py8
-rw-r--r--tests/queries/tests.py106
-rw-r--r--tests/queryset_pickle/models.py10
-rw-r--r--tests/queryset_pickle/tests.py43
-rw-r--r--tests/requests/tests.py2
-rwxr-xr-xtests/runtests.py15
-rw-r--r--tests/select_for_update/tests.py2
-rw-r--r--tests/serializers/tests.py3
-rw-r--r--tests/servers/tests.py9
-rw-r--r--tests/settings_tests/tests.py2
-rw-r--r--tests/shared_models/models.py37
-rw-r--r--tests/signals_regress/models.py18
-rw-r--r--tests/signals_regress/tests.py6
-rw-r--r--tests/swappable_models/tests.py7
-rw-r--r--tests/syncdb_signals/tests.py7
-rw-r--r--tests/test_runner/tests.py22
-rw-r--r--tests/test_suite_override/tests.py5
-rw-r--r--tests/test_utils/tests.py6
-rw-r--r--tests/transactions/tests.py16
-rw-r--r--tests/transactions_regress/tests.py21
-rw-r--r--tests/urlpatterns_reverse/tests.py14
-rw-r--r--tests/validation/models.py1
-rw-r--r--tests/validation/tests.py6
-rw-r--r--tests/validators/tests.py6
-rw-r--r--tests/view_tests/tests/test_i18n.py2
161 files changed, 3126 insertions, 1218 deletions
diff --git a/.gitignore b/.gitignore
index 0ad4e34e23..e95542e3ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,6 @@
MANIFEST
dist/
docs/_build/
+docs/locale/
tests/coverage_html/
tests/.coverage
diff --git a/AUTHORS b/AUTHORS
index 80f277996b..3c0cb7a7b5 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -45,6 +45,7 @@ The PRIMARY AUTHORS are (and/or have been):
* Donald Stufft
* Daniel Lindsley
* Marc Tamlyn
+ * Baptiste Mispelon
More information on the main contributors to Django can be found in
docs/internals/committers.txt.
@@ -413,7 +414,6 @@ answer newbie questions, and generally made Django that much better:
Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com>
- Baptiste Mispelon <bmispelon@gmail.com>
mitakummaa@gmail.com
Taylor Mitchell <taylor.mitchell@gmail.com>
mmarshall
@@ -550,6 +550,7 @@ answer newbie questions, and generally made Django that much better:
Thomas Steinacher <http://www.eggdrop.ch/>
Emil Stenström <em@kth.se>
Johan C. Stöver <johan@nilling.nl>
+ Chris Streeter <chris@chrisstreeter.com>
Nowell Strite <http://nowell.strite.org/>
Thomas Stromberg <tstromberg@google.com>
Hannes Struß <x@hannesstruss.de>
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/tests/shared_models/__init__.py b/django/core/compat_checks/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/tests/shared_models/__init__.py
+++ 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:
diff --git a/docs/Makefile b/docs/Makefile
index 2a8bcd7101..a2c926c7ea 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -6,11 +6,12 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
+LANGUAGE =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees -D language=$(LANGUAGE) $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
diff --git a/docs/conf.py b/docs/conf.py
index a01ddb60b8..feff99b6f4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -77,6 +77,9 @@ django_next_version = '1.6'
# for a list of supported languages.
#language = None
+# Location for .po/.mo translation files used when language is set
+locale_dirs = ['locale/']
+
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
diff --git a/docs/howto/deployment/wsgi/index.txt b/docs/howto/deployment/wsgi/index.txt
index ad32366bf8..17eb53cf0c 100644
--- a/docs/howto/deployment/wsgi/index.txt
+++ b/docs/howto/deployment/wsgi/index.txt
@@ -82,6 +82,14 @@ to combine a Django application with a WSGI application of another framework.
.. _`WSGI middleware`: http://www.python.org/dev/peps/pep-3333/#middleware-components-that-play-both-sides
+.. note::
+
+ Some third-party WSGI middleware do not call ``close`` on the response
+ object after handling a request — most notably Sentry's error reporting
+ middleware up to version 2.0.7. In those cases the
+ :data:`~django.core.signals.request_finished` signal isn't sent. This can
+ result in idle connections to database and memcache servers.
+
Upgrading from Django < 1.4
---------------------------
diff --git a/docs/howto/deployment/wsgi/uwsgi.txt b/docs/howto/deployment/wsgi/uwsgi.txt
index 22f39342d6..3e9b47e8f9 100644
--- a/docs/howto/deployment/wsgi/uwsgi.txt
+++ b/docs/howto/deployment/wsgi/uwsgi.txt
@@ -34,6 +34,15 @@ command. For example:
.. _installation procedures: http://projects.unbit.it/uwsgi/wiki/Install
+.. warning::
+
+ Some distributions, including Debian and Ubuntu, ship an outdated version
+ of uWSGI that does not conform to the WSGI specification. Versions prior to
+ 1.2.6 do not call ``close`` on the response object after handling a
+ request. In those cases the :data:`~django.core.signals.request_finished`
+ signal isn't sent. This can result in idle connections to database and
+ memcache servers.
+
uWSGI model
-----------
diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt
index b56c8e469b..6732f1561f 100644
--- a/docs/internals/committers.txt
+++ b/docs/internals/committers.txt
@@ -526,6 +526,17 @@ Marc Tamlyn
.. _CCBV: http://ccbv.co.uk/
.. _Incuna Ltd: http://incuna.com/
+Baptiste Mispelon
+ Baptiste discovered Django around the 1.2 version and promptly switched away
+ from his homegrown PHP framework. He started getting more involved in the
+ project after attending DjangoCon EU 2012, mostly by triaging tickets and
+ submitting small patches.
+
+ Baptiste currently lives in Budapest, Hungary and works for `M2BPO`_,
+ a small French company providing services to architects.
+
+.. _M2BPO: http://www.m2bpo.fr
+
Developers Emeritus
===================
diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt
index ed8aad99b3..8f8ddddab4 100644
--- a/docs/internals/contributing/writing-code/submitting-patches.txt
+++ b/docs/internals/contributing/writing-code/submitting-patches.txt
@@ -37,7 +37,7 @@ and time availability), claim it by following these steps:
`ticket tracker`_.
* If a ticket for this issue already exists, make sure nobody else has
- claimed it. To do this, look at the "Assigned to" section of the ticket.
+ claimed it. To do this, look at the "Owned by" section of the ticket.
If it's assigned to "nobody," then it's available to be claimed.
Otherwise, somebody else is working on this ticket, and you either find
another bug/feature to work on, or contact the developer working on the
@@ -48,7 +48,7 @@ and time availability), claim it by following these steps:
* Claim the ticket:
- 1. click the "accept" radio button under "Action" near the bottom of the
+ 1. click the "assign to myself" radio button under "Action" near the bottom of the
page,
2. then click "Submit changes."
diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt
index 120369172e..6193ec45f7 100644
--- a/docs/intro/tutorial03.txt
+++ b/docs/intro/tutorial03.txt
@@ -473,6 +473,13 @@ template for all 404 errors when :setting:`DEBUG` is set to ``False`` (in your
settings module). If you do create the template, add at least some dummy
content like "Page not found".
+.. warning::
+
+ If :setting:`DEBUG` is set to ``False``, all responses will be
+ "Bad Request (400)" unless you specify the proper :setting:`ALLOWED_HOSTS`
+ as well (something like ``['localhost', '127.0.0.1']`` for
+ local development).
+
A couple more things to note about 404 views:
* If :setting:`DEBUG` is set to ``True`` (in your settings module) then your
diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt
index 319bd4ebfe..4ad017c342 100644
--- a/docs/ref/class-based-views/base.txt
+++ b/docs/ref/class-based-views/base.txt
@@ -192,22 +192,24 @@ RedirectView
permanent = False
query_string = True
+ pattern_name = 'article-detail'
- def get_redirect_url(self, pk):
+ def get_redirect_url(self, *args, **kwargs):
article = get_object_or_404(Article, pk=pk)
article.update_counter()
- return reverse('product_detail', args=(pk,))
+ return super(ArticleCounterRedirectView, self).get_redirect_url(*args, **kwargs)
**Example urls.py**::
from django.conf.urls import patterns, url
from django.views.generic.base import RedirectView
- from article.views import ArticleCounterRedirectView
+ from article.views import ArticleCounterRedirectView, ArticleDetail
urlpatterns = patterns('',
- url(r'^(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'),
+ url(r'^counter/(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'),
+ url(r'^details/(?P<pk>\d+)/$', ArticleDetail.as_view(), name='article-detail'),
url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'),
)
@@ -218,6 +220,11 @@ RedirectView
The URL to redirect to, as a string. Or ``None`` to raise a 410 (Gone)
HTTP error.
+ .. attribute:: pattern_name
+
+ The name of the URL pattern to redirect to. Reversing will be done
+ using the same args and kwargs as are passed in for this view.
+
.. attribute:: permanent
Whether the redirect should be permanent. The only difference here is
diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt
index 1ebee254b1..205d556463 100644
--- a/docs/ref/class-based-views/generic-date-based.txt
+++ b/docs/ref/class-based-views/generic-date-based.txt
@@ -63,7 +63,7 @@ ArchiveIndexView
month or day using the attribute ``date_list_period``. This also applies
to all subclass views.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.conf.urls import patterns, url
from django.views.generic.dates import ArchiveIndexView
@@ -160,7 +160,7 @@ YearArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_year``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.dates import YearArchiveView
@@ -172,7 +172,7 @@ YearArchiveView
make_object_list = True
allow_future = True
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -255,7 +255,7 @@ MonthArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_month``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.dates import MonthArchiveView
@@ -267,7 +267,7 @@ MonthArchiveView
make_object_list = True
allow_future = True
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -348,7 +348,7 @@ WeekArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_week``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.dates import WeekArchiveView
@@ -361,7 +361,7 @@ WeekArchiveView
week_format = "%W"
allow_future = True
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -463,7 +463,7 @@ DayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_day``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.dates import DayArchiveView
@@ -475,7 +475,7 @@ DayArchiveView
make_object_list = True
allow_future = True
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -537,7 +537,7 @@ TodayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_today``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.dates import TodayArchiveView
@@ -549,7 +549,7 @@ TodayArchiveView
make_object_list = True
allow_future = True
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -599,7 +599,7 @@ DateDetailView
* Uses a default ``template_name_suffix`` of ``_detail``.
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
from django.views.generic.dates import DateDetailView
diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt
index c133134d65..7fc95ac5d2 100644
--- a/docs/ref/class-based-views/generic-display.txt
+++ b/docs/ref/class-based-views/generic-display.txt
@@ -36,7 +36,7 @@ DetailView
9. ``get()``
10. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.detail import DetailView
from django.utils import timezone
@@ -52,7 +52,7 @@ DetailView
context['now'] = timezone.now()
return context
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -62,6 +62,16 @@ DetailView
url(r'^(?P<slug>[-_\w]+)/$', ArticleDetailView.as_view(), name='article-detail'),
)
+ **Example myapp/article_detail.html**:
+
+ .. code-block:: html+django
+
+ <h1>{{ object.headline }}</h1>
+ <p>{{ object.content }}</p>
+ <p>Reporter: {{ object.reporter }}</p>
+ <p>Published: {{ object.pub_date|date }}</p>
+ <p>Date: {{ object.now|date }}</p>
+
ListView
--------
@@ -111,7 +121,7 @@ ListView
context['now'] = timezone.now()
return context
- **Example urls.py**::
+ **Example myapp/urls.py**::
from django.conf.urls import patterns, url
@@ -121,6 +131,19 @@ ListView
url(r'^$', ArticleListView.as_view(), name='article-list'),
)
+ **Example myapp/article_list.html**:
+
+ .. code-block:: html+django
+
+ <h1>Articles</h1>
+ <ul>
+ {% for article in object_list %}
+ <li>{{ article.pub_date|date }} - {{ article.headline }}</li>
+ {% empty %}
+ <li>No articles yet.</li>
+ {% endfor %}
+ </ul>
+
.. class:: django.views.generic.list.BaseListView
A base view for displaying a list of objects. It is not intended to be used
diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt
index c1fb2dcca9..51dfe7c71b 100644
--- a/docs/ref/class-based-views/generic-editing.txt
+++ b/docs/ref/class-based-views/generic-editing.txt
@@ -42,7 +42,7 @@ FormView
* :class:`django.views.generic.edit.ProcessFormView`
* :class:`django.views.generic.base.View`
- **Example forms.py**::
+ **Example myapp/forms.py**::
from django import forms
@@ -54,7 +54,7 @@ FormView
# send email using the self.cleaned_data dictionary
pass
- **Example views.py**::
+ **Example myapp/views.py**::
from myapp.forms import ContactForm
from django.views.generic.edit import FormView
@@ -70,6 +70,16 @@ FormView
form.send_email()
return super(ContactView, self).form_valid(form)
+ **Example myapp/contact.html**:
+
+ .. code-block:: html+django
+
+ <form action="" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Send message" />
+ </form>
+
+
CreateView
----------
@@ -101,7 +111,7 @@ CreateView
creating objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_create_form.html'``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.edit import CreateView
from myapp.models import Author
@@ -110,6 +120,15 @@ CreateView
model = Author
fields = ['name']
+ **Example myapp/author_form.html**:
+
+ .. code-block:: html+django
+
+ <form action="" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Create" />
+ </form>
+
UpdateView
----------
@@ -143,7 +162,7 @@ UpdateView
updating objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_update_form.html'``.
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.edit import UpdateView
from myapp.models import Author
@@ -151,6 +170,16 @@ UpdateView
class AuthorUpdate(UpdateView):
model = Author
fields = ['name']
+ template_name_suffix = '_update_form'
+
+ **Example myapp/author_update_form.html**:
+
+ .. code-block:: html+django
+
+ <form action="" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Update" />
+ </form>
DeleteView
----------
@@ -184,8 +213,7 @@ DeleteView
deleting objects for the example ``Author`` model would cause the
default ``template_name`` to be ``'myapp/author_check_delete.html'``.
-
- **Example views.py**::
+ **Example myapp/views.py**::
from django.views.generic.edit import DeleteView
from django.core.urlresolvers import reverse_lazy
@@ -194,3 +222,12 @@ DeleteView
class AuthorDelete(DeleteView):
model = Author
success_url = reverse_lazy('author-list')
+
+ **Example myapp/author_confirm_delete.html**:
+
+ .. code-block:: html+django
+
+ <form action="" method="post">{% csrf_token %}
+ <p>Are you sure you want to delete "{{ object }}"?</p>
+ <input type="submit" value="Confirm" />
+ </form>
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 7377f11a63..90ef68837a 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -870,6 +870,14 @@ subclass::
``prepopulated_fields`` doesn't accept ``DateTimeField``, ``ForeignKey``,
nor ``ManyToManyField`` fields.
+.. attribute:: ModelAdmin.preserve_filters
+
+ .. versionadded:: 1.6
+
+ The admin now preserves filters on the list view after creating, editing
+ or deleting an object. You can restore the previous behavior of clearing
+ filters by setting this attribute to ``False``.
+
.. attribute:: ModelAdmin.radio_fields
By default, Django's admin uses a select-box interface (<select>) for
@@ -2269,9 +2277,9 @@ your URLconf. Specifically, add these four patterns:
.. code-block:: python
url(r'^admin/password_reset/$', 'django.contrib.auth.views.password_reset', name='admin_password_reset'),
- (r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done'),
- (r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm'),
- (r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete'),
+ url(r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'),
+ url(r'^reset/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$', 'django.contrib.auth.views.password_reset_confirm', name='password_reset_confirm'),
+ url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'),
(This assumes you've added the admin at ``admin/`` and requires that you put
the URLs starting with ``^admin/`` before the line that includes the admin app
diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt
index 40b3629f63..afbc6ec048 100644
--- a/docs/ref/contrib/auth.txt
+++ b/docs/ref/contrib/auth.txt
@@ -132,12 +132,28 @@ Methods
password hashing. Doesn't save the
:class:`~django.contrib.auth.models.User` object.
+ When the ``raw_password`` is ``None``, the password will be set to an
+ unusable password, as if
+ :meth:`~django.contrib.auth.models.User.set_unusable_password()`
+ were used.
+
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally stored
+ as an unsable password.
+
.. method:: check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
comparison.)
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally
+ considered to be an unusable password, resulting in this method
+ returning ``False`` for such a password.
+
.. method:: set_unusable_password()
Marks the user as having no password set. This isn't the same as
diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt
index 292b304acb..11d74d75c3 100644
--- a/docs/ref/contrib/flatpages.txt
+++ b/docs/ref/contrib/flatpages.txt
@@ -47,7 +47,7 @@ Then either:
3. Add an entry in your URLconf. For example::
urlpatterns = patterns('',
- ('^pages/', include('django.contrib.flatpages.urls')),
+ (r'^pages/', include('django.contrib.flatpages.urls')),
)
or:
@@ -74,7 +74,7 @@ There are several ways to include the flat pages in your URLconf. You can
dedicate a particular path to flat pages::
urlpatterns = patterns('',
- ('^pages/', include('django.contrib.flatpages.urls')),
+ (r'^pages/', include('django.contrib.flatpages.urls')),
)
You can also set it up as a "catchall" pattern. In this case, it is important
@@ -82,9 +82,15 @@ to place the pattern at the end of the other urlpatterns::
# Your other patterns here
urlpatterns += patterns('django.contrib.flatpages.views',
- (r'^(?P<url>.*)$', 'flatpage'),
+ (r'^(?P<url>.*/)$', 'flatpage'),
)
+.. warning::
+
+ If you set :setting:`APPEND_SLASH` to ``False``, you must remove the slash
+ in the catchall pattern or flatpages without a trailing slash will not be
+ matched.
+
Another common setup is to use flat pages for a limited set of known pages and
to hard code the urls, so you can reference them with the :ttag:`url` template
tag::
diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt
index 7795a32c09..ca11aef763 100644
--- a/docs/ref/contrib/formtools/form-wizard.txt
+++ b/docs/ref/contrib/formtools/form-wizard.txt
@@ -166,7 +166,7 @@ This template expects a ``wizard`` object that has various items attached to
it:
* ``form`` -- The :class:`~django.forms.Form` or
- :class:`~django.forms.formset.BaseFormSet` instance for the current step
+ :class:`~django.forms.formsets.BaseFormSet` instance for the current step
(either empty or with errors).
* ``steps`` -- A helper object to access the various steps related data:
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index e21e3d2766..c2c11e6db4 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -89,6 +89,19 @@ documentation for the :djadminopt:`--verbosity` option.
Available commands
==================
+checksetup
+----------
+
+.. django-admin:: checksetup
+
+.. versionadded:: 1.6
+
+Performs a series of checks to verify a given setup (settings/application code)
+is compatible with the current version of Django.
+
+Upon finding things that are incompatible or require notifying the user, it
+issues a series of warnings.
+
cleanup
-------
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 67e3aab712..3c17827800 100644
--- a/docs/ref/forms/api.txt
+++ b/docs/ref/forms/api.txt
@@ -498,6 +498,8 @@ include ``%s`` -- then the library will act as if ``auto_id`` is ``True``.
By default, ``auto_id`` is set to the string ``'id_%s'``.
+.. attribute:: Form.label_suffix
+
Normally, a colon (``:``) will be appended after any label name when a form is
rendered. It's possible to change the colon to another character, or omit it
entirely, using the ``label_suffix`` parameter::
@@ -650,12 +652,17 @@ To separately render the label tag of a form field, you can call its
>>> f = ContactForm(data)
>>> print(f['message'].label_tag())
- <label for="id_message">Message</label>
+ <label for="id_message">Message:</label>
Optionally, you can provide the ``contents`` parameter which will replace the
auto-generated label tag. An optional ``attrs`` dictionary may contain
additional attributes for the ``<label>`` tag.
+.. versionchanged:: 1.6
+
+ The label now includes the form's :attr:`~django.forms.Form.label_suffix`
+ (a semicolon, by default).
+
.. method:: BoundField.css_classes()
When you use Django's rendering shortcuts, CSS classes are used to
@@ -688,6 +695,29 @@ by a ``Widget``::
>>> print(bound_form['subject'].value())
hi
+.. attribute:: BoundField.id_for_label
+
+Use this property to render the ID of this field. For example, if you are
+manually constructing a ``<label>`` in your template (despite the fact that
+:meth:`~BoundField.label_tag` will do this for you):
+
+.. code-block:: html+django
+
+ <label for="{{ form.my_field.id_for_label }}">...</label>{{ my_field }}
+
+By default, this will be the field's name prefixed by ``id_``
+("``id_my_field``" for the example above). You may modify the ID by setting
+:attr:`~django.forms.Widget.attrs` on the field's widget. For example,
+declaring a field like this::
+
+ my_field = forms.CharField(widget=forms.TextInput(attrs={'id': 'myFIELD'}))
+
+and using the template above, would render something like:
+
+.. code-block:: html
+
+ <label for="myFIELD">...</label><input id="myFIELD" type="text" name="my_field" />
+
.. _binding-uploaded-files:
Binding uploaded files to a form
diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt
index b54056af0c..840a896d75 100644
--- a/docs/ref/forms/models.txt
+++ b/docs/ref/forms/models.txt
@@ -5,7 +5,7 @@ Model Form Functions
.. module:: django.forms.models
:synopsis: Django's functions for building model forms and formsets.
-.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None)
+.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None)
Returns a :class:`~django.forms.ModelForm` class for the given ``model``.
You can optionally pass a ``form`` argument to use as a starting point for
@@ -20,11 +20,18 @@ Model Form Functions
``widgets`` is a dictionary of model field names mapped to a widget.
- ``localized_fields`` is a list of names of fields which should be localized.
-
``formfield_callback`` is a callable that takes a model field and returns
a form field.
+ ``localized_fields`` is a list of names of fields which should be localized.
+
+ ``labels`` is a dictionary of model field names mapped to a label.
+
+ ``help_texts`` is a dictionary of model field names mapped to a help text.
+
+ ``error_messages`` is a dictionary of model field names mapped to a
+ dictionary of error messages.
+
See :ref:`modelforms-factory` for example usage.
.. versionchanged:: 1.6
@@ -35,14 +42,16 @@ Model Form Functions
information. Omitting any definition of the fields to use will result in all
fields being used, but this behavior is deprecated.
- The ``localized_fields`` parameter was added.
+ The ``localized_fields``, ``labels``, ``help_texts``, and
+ ``error_messages`` parameters were added.
-.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None)
+.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None)
Returns a ``FormSet`` class for the given ``model`` class.
Arguments ``model``, ``form``, ``fields``, ``exclude``,
- ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to
+ ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``,
+ ``help_texts``, and ``error_messages`` are all passed through to
:func:`~django.forms.models.modelform_factory`.
Arguments ``formset``, ``extra``, ``max_num``, ``can_order``,
@@ -54,9 +63,10 @@ Model Form Functions
.. versionchanged:: 1.6
- The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.
+ The ``widgets``, ``validate_max``, ``localized_fields``, ``labels``,
+ ``help_texts``, and ``error_messages`` parameters were added.
-.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None)
+.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None)
Returns an ``InlineFormSet`` using :func:`modelformset_factory` with
defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and
@@ -69,4 +79,5 @@ Model Form Functions
.. versionchanged:: 1.6
- The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added.
+ The ``widgets``, ``validate_max`` and ``localized_fields``, ``labels``,
+ ``help_texts``, and ``error_messages`` parameters were added.
diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt
index 87c9764f64..8ab1c26831 100644
--- a/docs/ref/forms/validation.txt
+++ b/docs/ref/forms/validation.txt
@@ -12,13 +12,11 @@ validation (accessing the ``errors`` attribute or calling ``full_clean()``
directly), but normally they won't be needed.
In general, any cleaning method can raise ``ValidationError`` if there is a
-problem with the data it is processing, passing the relevant error message to
-the ``ValidationError`` constructor. If no ``ValidationError`` is raised, the
-method should return the cleaned (normalized) data as a Python object.
-
-If you detect multiple errors during a cleaning method and wish to signal all
-of them to the form submitter, it is possible to pass a list of errors to the
-``ValidationError`` constructor.
+problem with the data it is processing, passing the relevant information to
+the ``ValidationError`` constructor. :ref:`See below <raising-validation-error>`
+for the best practice in raising ``ValidationError``. If no ``ValidationError``
+is raised, the method should return the cleaned (normalized) data as a Python
+object.
Most validation can be done using `validators`_ - simple helpers that can be
reused easily. Validators are simple functions (or callables) that take a single
@@ -87,7 +85,8 @@ overridden:
"field" (called ``__all__``), which you can access via the
``non_field_errors()`` method if you need to. If you want to attach
errors to a specific field in the form, you will need to access the
- ``_errors`` attribute on the form, which is `described later`_.
+ ``_errors`` attribute on the form, which is
+ :ref:`described later <modifying-field-errors>`.
Also note that there are special considerations when overriding
the ``clean()`` method of a ``ModelForm`` subclass. (see the
@@ -116,7 +115,100 @@ should iterate through ``self.cleaned_data.items()``, possibly considering the
``_errors`` dictionary attribute on the form as well. In this way, you will
already know which fields have passed their individual validation requirements.
-.. _described later:
+.. _raising-validation-error:
+
+Raising ``ValidationError``
+---------------------------
+
+.. versionchanged:: 1.6
+
+In order to make error messages flexible and easy to override, consider the
+following guidelines:
+
+* Provide a descriptive error ``code`` to the constructor::
+
+ # Good
+ ValidationError(_('Invalid value'), code='invalid')
+
+ # Bad
+ ValidationError(_('Invalid value'))
+
+* Don't coerce variables into the message; use placeholders and the ``params``
+ argument of the constructor::
+
+ # Good
+ ValidationError(
+ _('Invalid value: %(value)s'),
+ params={'value': '42'},
+ )
+
+ # Bad
+ ValidationError(_('Invalid value: %s') % value)
+
+* Use mapping keys instead of positional formatting. This enables putting
+ the variables in any order or omitting them altogether when rewriting the
+ message::
+
+ # Good
+ ValidationError(
+ _('Invalid value: %(value)s'),
+ params={'value': '42'},
+ )
+
+ # Bad
+ ValidationError(
+ _('Invalid value: %s'),
+ params=('42',),
+ )
+
+* Wrap the message with ``gettext`` to enable translation::
+
+ # Good
+ ValidationError(_('Invalid value'))
+
+ # Bad
+ ValidationError('Invalid value')
+
+Putting it all together::
+
+ raise ValidationErrror(
+ _('Invalid value: %(value)s'),
+ code='invalid',
+ params={'value': '42'},
+ )
+
+Following these guidelines is particularly necessary if you write reusable
+forms, form fields, and model fields.
+
+While not recommended, if you are at the end of the validation chain
+(i.e. your form ``clean()`` method) and you know you will *never* need
+to override your error message you can still opt for the less verbose::
+
+ ValidationError(_('Invalid value: %s') % value)
+
+Raising multiple errors
+~~~~~~~~~~~~~~~~~~~~~~~
+
+If you detect multiple errors during a cleaning method and wish to signal all
+of them to the form submitter, it is possible to pass a list of errors to the
+``ValidationError`` constructor.
+
+As above, it is recommended to pass a list of ``ValidationError`` instances
+with ``code``\s and ``params`` but a list of strings will also work::
+
+ # Good
+ raise ValidationError([
+ ValidationError(_('Error 1'), code='error1'),
+ ValidationError(_('Error 2'), code='error2'),
+ ])
+
+ # Bad
+ raise ValidationError([
+ _('Error 1'),
+ _('Error 2'),
+ ])
+
+.. _modifying-field-errors:
Form subclasses and modifying field errors
------------------------------------------
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index 0f6917d44c..341574ad37 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -163,6 +163,9 @@ Django will then include the extra attributes in the rendered output:
<tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
+You can also set the HTML ``id`` using :attr:`~Widget.attrs`. See
+:attr:`BoundField.id_for_label` for an example.
+
.. _styling-widget-classes:
Styling widget classes
@@ -251,7 +254,7 @@ foundation for custom widgets.
into two separate values::
from django.forms import MultiWidget
-
+
class SplitDateTimeWidget(MultiWidget):
# ...
diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
index f989ff1bec..cfc95db092 100644
--- a/docs/ref/models/instances.txt
+++ b/docs/ref/models/instances.txt
@@ -84,12 +84,18 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle
validation errors yourself, or if you have excluded fields from the
:class:`~django.forms.ModelForm` that require validation.
-.. method:: Model.full_clean(exclude=None)
+.. method:: Model.full_clean(exclude=None, validate_unique=True)
+
+.. versionchanged:: 1.6
+
+ The ``validate_unique`` parameter was added to allow skipping
+ :meth:`Model.validate_unique()`. Previously, :meth:`Model.validate_unique()`
+ was always called by ``full_clean``.
This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and
-:meth:`Model.validate_unique()`, in that order and raises a
-:exc:`~django.core.exceptions.ValidationError` that has a ``message_dict``
-attribute containing errors from all three stages.
+:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``, in that
+order and raises a :exc:`~django.core.exceptions.ValidationError` that has a
+``message_dict`` attribute containing errors from all three stages.
The optional ``exclude`` argument can be used to provide a list of field names
that can be excluded from validation and cleaning.
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index ef52d3170c..897af275a0 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -852,6 +852,10 @@ It is also important to remember that when running with :setting:`DEBUG`
turned on, Django will remember every SQL query it executes. This is useful
when you're debugging, but it'll rapidly consume memory on a production server.
+Finally, if :setting:`DEBUG` is ``False``, you also need to properly set
+the :setting:`ALLOWED_HOSTS` setting. Failing to do so will result in all
+requests being returned as "Bad Request (400)".
+
.. _django/views/debug.py: https://github.com/django/django/blob/master/django/views/debug.py
.. setting:: DEBUG_PROPAGATE_EXCEPTIONS
diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt
index e7270e1957..06ba2cb3e8 100644
--- a/docs/ref/signals.txt
+++ b/docs/ref/signals.txt
@@ -498,17 +498,20 @@ request_finished
Sent when Django finishes processing an HTTP request.
-.. note::
+.. versionchanged:: 1.5
- When a view returns a :ref:`streaming response <httpresponse-streaming>`,
- this signal is sent only after the entire response is consumed by the
- client (strictly speaking, by the WSGI gateway).
+ Before Django 1.5, this signal was sent before delivering content to the
+ client. In order to accommodate :ref:`streaming responses
+ <httpresponse-streaming>`, it is now sent after the response has been fully
+ delivered to the client.
-.. versionchanged:: 1.5
+.. note::
- Before Django 1.5, this signal was fired before sending the content to the
- client. In order to accomodate streaming responses, it is now fired after
- sending the content.
+ Some WSGI servers and middleware do not always call ``close`` on the
+ response object after handling a request, most notably uWSGI prior to 1.2.6
+ and Sentry's error reporting middleware up to 2.0.7. In those cases this
+ signal isn't sent at all. This can result in idle connections to database
+ and memcache servers.
Arguments sent with this signal:
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 24eda2ce2c..afb6e4b123 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -371,7 +371,7 @@ displayed if the given array is empty or could not be found::
{% for athlete in athlete_list %}
<li>{{ athlete.name }}</li>
{% empty %}
- <li>Sorry, no athlete in this list!</li>
+ <li>Sorry, no athletes in this list.</li>
{% endfor %}
<ul>
@@ -2401,7 +2401,7 @@ It is also able to consume standard context variables, e.g. assuming a
<link rel="stylesheet" href="{% static user_stylesheet %}" type="text/css" media="screen" />
If you'd like to retrieve a static URL without displaying it, you can use a
-slightly different call::
+slightly different call:
.. versionadded:: 1.5
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index d2ef945a2e..45d7781403 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -659,6 +659,8 @@ Functions for working with Python modules.
.. function:: import_by_path(dotted_path, error_prefix='')
+ .. versionadded:: 1.6
+
Imports a dotted module path and returns the attribute/class designated by
the last name in the path. Raises
:exc:`~django.core.exceptions.ImproperlyConfigured` if something goes
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index c54373ee41..a1b046776b 100644
--- a/docs/releases/1.5.txt
+++ b/docs/releases/1.5.txt
@@ -440,7 +440,15 @@ generation.
This signal is now sent after the content is fully consumed by the WSGI
gateway. This might be backwards incompatible if you rely on the signal being
fired before sending the response content to the client. If you do, you should
-consider using a middleware instead.
+consider using :doc:`middleware </topics/http/middleware>` instead.
+
+.. note::
+
+ Some WSGI servers and middleware do not always call ``close`` on the
+ response object after handling a request, most notably uWSGI prior to 1.2.6
+ and Sentry's error reporting middleware up to 2.0.7. In those cases the
+ ``request_finished`` signal isn't sent at all. This can result in idle
+ connections to database and memcache servers.
OPTIONS, PUT and DELETE requests in the test client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index 6736af8c2d..bd6255eae6 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -121,6 +121,13 @@ GeoDjango now provides :ref:`form fields and widgets <ref-gis-forms-api>` for
its geo-specialized fields. They are OpenLayers-based by default, but they can
be customized to use any other JS framework.
+``checksetup`` management command added for verifying compatibility
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A ``checksetup`` management command was added, enabling you to verify if your
+current configuration (currently oriented at settings) is compatible with the
+current version of Django.
+
Minor features
~~~~~~~~~~~~~~
@@ -236,9 +243,14 @@ Minor features
.. _`Pillow`: https://pypi.python.org/pypi/Pillow
.. _`PIL`: https://pypi.python.org/pypi/PIL
-* :doc:`ModelForm </topics/forms/modelforms/>` accepts a new
- Meta option: ``localized_fields``. Fields included in this list will be localized
- (by setting ``localize`` on the form field).
+* :class:`~django.forms.ModelForm` accepts several new ``Meta``
+ options.
+
+ * Fields included in the ``localized_fields`` list will be localized
+ (by setting ``localize`` on the form field).
+ * The ``labels``, ``help_texts`` and ``error_messages`` options may be used
+ to customize the default fields, see
+ :ref:`modelforms-overriding-default-fields` for details.
* The ``choices`` argument to model fields now accepts an iterable of iterables
instead of requiring an iterable of lists or tuples.
@@ -303,6 +315,21 @@ Minor features
:class:`~django.contrib.admin.InlineModelAdmin` may be overridden to
customize the extra and maximum number of inline forms.
+* Formsets now have a
+ :meth:`~django.forms.formsets.BaseFormSet.total_error_count` method.
+
+* :class:`~django.forms.ModelForm` fields can now override error messages
+ defined in model fields by using the
+ :attr:`~django.forms.Field.error_messages` argument of a ``Field``'s
+ constructor. To take advantage of this new feature with your custom fields,
+ :ref:`see the updated recommendation <raising-validation-error>` for raising
+ a ``ValidationError``.
+
+* :class:`~django.contrib.admin.ModelAdmin` now preserves filters on the list view
+ after creating, editing or deleting an object. It's possible to restore the previous
+ behavior of clearing filters by setting the
+ :attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``.
+
Backwards incompatible changes in 1.6
=====================================
@@ -581,6 +608,47 @@ It is still possible to convert the fetched rows to ``Model`` objects
lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()`
method.
+:meth:`BoundField.label_tag<django.forms.BoundField.label_tag>` now includes the form's :attr:`~django.forms.Form.label_suffix`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is consistent with how methods like
+:meth:`Form.as_p<django.forms.Form.as_p>` and
+:meth:`Form.as_ul<django.forms.Form.as_ul>` render labels.
+
+If you manually render ``label_tag`` in your templates:
+
+.. code-block:: html+django
+
+ {{ form.my_field.label_tag }}: {{ form.my_field }}
+
+you'll want to remove the semicolon (or whatever other separator you may be
+using) to avoid duplicating it when upgrading to Django 1.6. The following
+template in Django 1.6 will render identically to the above template in Django
+1.5, except that the semicolon will appear inside the ``<label>`` element.
+
+.. code-block:: html+django
+
+ {{ form.my_field.label_tag }} {{ form.my_field }}
+
+will render something like:
+
+.. code-block:: html
+
+ <label for="id_my_field">My Field:</label> <input id="id_my_field" type="text" name="my_field" />
+
+If you want to keep the current behavior of rendering ``label_tag`` without
+the ``label_suffix``, instantiate the form ``label_suffix=''``.
+
+Admin views ``_changelist_filters`` GET parameter
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To achieve preserving and restoring list view filters, admin views now
+pass around the `_changelist_filters` GET parameter. It's important that you
+account for that change if you have custom admin templates or if your tests
+rely on the previous URLs. If you want to revert to the original behavior you
+can set the
+:attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``.
+
Miscellaneous
~~~~~~~~~~~~~
@@ -634,6 +702,29 @@ Miscellaneous
:meth:`django.contrib.auth.logout` which will send the
:func:`~django.contrib.auth.signals.user_logged_out` signal.
+* :ref:`Authentication views <built-in-auth-views>` are now reversed by name,
+ not their locations in ``django.contrib.auth.views``. If you are using the
+ views without a ``name``, you should update your ``urlpatterns`` to use
+ :meth:`~django.conf.urls.url` with the ``name`` parameter. For example::
+
+ (r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete')
+
+ becomes::
+
+ url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete')
+
+* :class:`~django.views.generic.base.RedirectView` now has a `pattern_name`
+ attribute which allows it to choose the target by reversing the URL.
+
+* In Django 1.4 and 1.5, a blank string was unintentionally not considered to
+ be a valid password. This meant
+ :meth:`~django.contrib.auth.models.User.set_password()` would save a blank
+ password as an unusable password like
+ :meth:`~django.contrib.auth.models.User.set_unusable_password()` does, and
+ thus :meth:`~django.contrib.auth.models.User.check_password()` always
+ returned ``False`` for blank passwords. This has been corrected in this
+ release: blank passwords are now valid.
+
Features deprecated in 1.6
==========================
diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt
index bc021b14ad..ee1aedbd8c 100644
--- a/docs/topics/auth/customizing.txt
+++ b/docs/topics/auth/customizing.txt
@@ -37,7 +37,7 @@ plug in other authentication sources. You can override Django's default
database-based scheme, or you can use the default system in tandem with other
systems.
-See the `authentication backend reference
+See the :ref:`authentication backend reference
<authentication-backends-reference>` for information on the authentication
backends included with Django.
@@ -583,12 +583,28 @@ The following methods are available on any subclass of
password hashing. Doesn't save the
:class:`~django.contrib.auth.models.AbstractBaseUser` object.
+ When the raw_password is ``None``, the password will be set to an
+ unusable password, as if
+ :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()`
+ were used.
+
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally stored
+ as an unsable password as well.
+
.. method:: models.AbstractBaseUser.check_password(raw_password)
Returns ``True`` if the given raw string is the correct password for
the user. (This takes care of the password hashing in making the
comparison.)
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally
+ considered to be an unusable password, resulting in this method
+ returning ``False`` for such a password.
+
.. method:: models.AbstractBaseUser.set_unusable_password()
Marks the user as having no password set. This isn't the same as
diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt
index 206e7d856c..7e4b59a99c 100644
--- a/docs/topics/auth/passwords.txt
+++ b/docs/topics/auth/passwords.txt
@@ -206,6 +206,12 @@ from the ``User`` model.
database to check against, and returns ``True`` if they match, ``False``
otherwise.
+ .. versionchanged:: 1.6
+
+ In Django 1.4 and 1.5, a blank string was unintentionally considered
+ to be an unusable password, resulting in this method returning
+ ``False`` for such a password.
+
.. function:: make_password(password[, salt, hashers])
Creates a hashed password in the format used by this application. It takes
diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt
index 980e571c85..3a4811e7bb 100644
--- a/docs/topics/class-based-views/mixins.txt
+++ b/docs/topics/class-based-views/mixins.txt
@@ -233,7 +233,7 @@ We'll demonstrate this with the publisher modelling we used in the
from django.views.generic.detail import SingleObjectMixin
from books.models import Author
- class RecordInterest(View, SingleObjectMixin):
+ class RecordInterest(SingleObjectMixin, View):
"""Records the current user's interest in an author."""
model = Author
@@ -446,7 +446,7 @@ Our new ``AuthorDetail`` looks like this::
class AuthorInterestForm(forms.Form):
message = forms.CharField()
- class AuthorDetail(DetailView, FormMixin):
+ class AuthorDetail(FormMixin, DetailView):
model = Author
form_class = AuthorInterestForm
@@ -553,7 +553,7 @@ template as ``AuthorDisplay`` is using on ``GET``.
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin
- class AuthorInterest(FormView, SingleObjectMixin):
+ class AuthorInterest(SingleObjectMixin, FormView):
template_name = 'books/author_detail.html'
form_class = AuthorInterestForm
model = Author
diff --git a/docs/topics/files.txt b/docs/topics/files.txt
index 492e6a20b5..45f83d8746 100644
--- a/docs/topics/files.txt
+++ b/docs/topics/files.txt
@@ -87,9 +87,9 @@ The following approach may be used to close files automatically::
# Create a Python file object using open() and the with statement
>>> with open('/tmp/hello.world', 'w') as f:
- >>> myfile = File(f)
- >>> for line in myfile:
- >>> print line
+ ... myfile = File(f)
+ ... myfile.write('Hello World')
+ ...
>>> myfile.closed
True
>>> f.closed
diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt
index 8745e9761d..29139c5dea 100644
--- a/docs/topics/forms/formsets.txt
+++ b/docs/topics/forms/formsets.txt
@@ -3,7 +3,7 @@
Formsets
========
-.. class:: django.forms.formset.BaseFormSet
+.. class:: django.forms.formsets.BaseFormSet
A formset is a layer of abstraction to work with multiple forms on the same
page. It can be best compared to a data grid. Let's say you have the following
@@ -164,6 +164,23 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the
forms in the formset. Validation was performed for each of the two forms, and
the expected error message appears for the second item.
+.. currentmodule:: django.forms.formsets.BaseFormSet
+
+.. method:: total_error_count(self)
+
+.. versionadded:: 1.6
+
+To check how many errors there are in the formset, we can use the
+``total_error_count`` method::
+
+ >>> # Using the previous example
+ >>> formset.errors
+ [{}, {'pub_date': [u'This field is required.']}]
+ >>> len(formset.errors)
+ 2
+ >>> formset.total_error_count()
+ 1
+
We can also check if form data differs from the initial data (i.e. the form was
sent without any data)::
@@ -247,8 +264,7 @@ is where you define your own validation that works at the formset level::
... # Don't bother validating the formset unless each form is valid on its own
... return
... titles = []
- ... for i in range(0, self.total_form_count()):
- ... form = self.forms[i]
+ ... for form in self.forms:
... title = form.cleaned_data['title']
... if title in titles:
... raise forms.ValidationError("Articles in a set must have distinct titles.")
diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index ac58acc9e3..7d5a0ed411 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -302,7 +302,7 @@ loop::
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
</div>
{% endfor %}
<p><input type="submit" value="Send message" /></p>
@@ -316,8 +316,14 @@ attributes, which can be useful in your templates:
The label of the field, e.g. ``Email address``.
``{{ field.label_tag }}``
- The field's label wrapped in the appropriate HTML ``<label>`` tag,
- e.g. ``<label for="id_email">Email address</label>``
+ The field's label wrapped in the appropriate HTML ``<label>`` tag.
+
+ .. versionchanged:: 1.6
+
+ This includes the form's :attr:`~django.forms.Form.label_suffix`. For
+ example, the default ``label_suffix`` is a semicolon::
+
+ <label for="id_email">Email address:</label>
``{{ field.value }}``
The value of the field. e.g ``someone@example.com``
@@ -375,7 +381,7 @@ these two methods::
{% for field in form.visible_fields %}
<div class="fieldWrapper">
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
</div>
{% endfor %}
<p><input type="submit" value="Send message" /></p>
@@ -403,7 +409,7 @@ using the :ttag:`include` tag to reuse it in other templates::
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
</div>
{% endfor %}
diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt
index b014e97119..a1fd533241 100644
--- a/docs/topics/forms/media.txt
+++ b/docs/topics/forms/media.txt
@@ -49,7 +49,7 @@ define the media requirements.
Here's a simple example::
- from django import froms
+ from django import forms
class CalendarWidget(forms.TextInput):
class Media:
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 4c46c6c0c0..bd9e14aea4 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -141,7 +141,7 @@ In addition, each generated form field has attributes set as follows:
``default`` value will be initially selected instead).
Finally, note that you can override the form field used for a given model
-field. See `Overriding the default field types or widgets`_ below.
+field. See `Overriding the default fields`_ below.
A full example
--------------
@@ -388,8 +388,10 @@ include that field.
.. _section on saving forms: `The save() method`_
-Overriding the default field types or widgets
----------------------------------------------
+.. _modelforms-overriding-default-fields:
+
+Overriding the default fields
+-----------------------------
The default field types, as described in the `Field types`_ table above, are
sensible defaults. If you have a ``DateField`` in your model, chances are you'd
@@ -420,38 +422,65 @@ widget::
The ``widgets`` dictionary accepts either widget instances (e.g.,
``Textarea(...)``) or classes (e.g., ``Textarea``).
-If you want to further customize a field -- including its type, label, etc. --
-you can do this by declaratively specifying fields like you would in a regular
-``Form``. Declared fields will override the default ones generated by using the
-``model`` attribute.
+.. versionadded:: 1.6
+
+ The ``labels``, ``help_texts`` and ``error_messages`` options were added.
+
+Similarly, you can specify the ``labels``, ``help_texts`` and ``error_messages``
+attributes of the inner ``Meta`` class if you want to further customize a field.
-For example, if you wanted to use ``MyDateFormField`` for the ``pub_date``
+For example if you wanted to customize the wording of all user facing strings for
+the ``name`` field::
+
+ class AuthorForm(ModelForm):
+ class Meta:
+ model = Author
+ fields = ('name', 'title', 'birth_date')
+ labels = {
+ 'name': _('Writer'),
+ }
+ help_texts = {
+ 'name': _('Some useful help text.'),
+ }
+ error_messages = {
+ 'name': {
+ 'max_length': _("This writer's name is too long."),
+ },
+ }
+
+Finally, if you want complete control over of a field -- including its type,
+validators, etc. -- you can do this by declaratively specifying fields like you
+would in a regular ``Form``. Declared fields will override the default ones
+generated by using the ``model`` attribute. Fields declared like this will
+ignore any customizations in the ``widgets``, ``labels``, ``help_texts``, and
+``error_messages`` options declared on ``Meta``.
+
+For example, if you wanted to use ``MySlugFormField`` for the ``slug``
field, you could do the following::
from django.forms import ModelForm
from myapp.models import Article
class ArticleForm(ModelForm):
- pub_date = MyDateFormField()
+ slug = MySlugFormField()
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter']
-If you want to override a field's default label, then specify the ``label``
-parameter when declaring the form field::
+If you want to override a field's default validators, then specify the
+``validators`` parameter when declaring the form field::
from django.forms import ModelForm, DateField
from myapp.models import Article
class ArticleForm(ModelForm):
- pub_date = DateField(label='Publication date')
+ slug = CharField(validators=[validate_slug])
class Meta:
model = Article
- fields = ['pub_date', 'headline', 'content', 'reporter']
-
+ fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
.. note::
@@ -597,7 +626,7 @@ example by specifying the widgets to be used for a given field::
>>> from django.forms import Textarea
>>> Form = modelform_factory(Book, form=BookForm,
- widgets={"title": Textarea()})
+ ... widgets={"title": Textarea()})
The fields to include can be specified using the ``fields`` and ``exclude``
keyword arguments, or the corresponding attributes on the ``ModelForm`` inner
@@ -907,7 +936,7 @@ Third, you can manually render each field::
{{ formset.management_form }}
{% for form in formset %}
{% for field in form %}
- {{ field.label_tag }}: {{ field }}
+ {{ field.label_tag }} {{ field }}
{% endfor %}
{% endfor %}
</form>
diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt
index fa26297988..1cfcd262cd 100644
--- a/docs/topics/settings.txt
+++ b/docs/topics/settings.txt
@@ -17,6 +17,11 @@ Here are a couple of example settings::
DEFAULT_FROM_EMAIL = 'webmaster@example.com'
TEMPLATE_DIRS = ('/home/templates/mike', '/home/templates/john')
+.. note::
+
+ If you set :setting:`DEBUG` to ``False``, you also need to properly set
+ the :setting:`ALLOWED_HOSTS` setting.
+
Because a settings file is a Python module, the following apply:
* It doesn't allow for Python syntax errors.
diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt
index b7f49d2b97..8f7f093901 100644
--- a/docs/topics/testing/advanced.txt
+++ b/docs/topics/testing/advanced.txt
@@ -155,6 +155,80 @@ If there are any circular dependencies in the
:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
exception will be raised.
+Advanced features of ``TransactionTestCase``
+============================================
+
+.. currentmodule:: django.test
+
+.. attribute:: TransactionTestCase.available_apps
+
+ .. versionadded:: 1.6
+
+ .. warning::
+
+ This attribute is a private API. It may be changed or removed without
+ a deprecation period in the future, for instance to accomodate changes
+ in application loading.
+
+ It's used to optimize Django's own test suite, which contains hundreds
+ of models but no relations between models in different applications.
+
+ By default, ``available_apps`` is set to ``None``. After each test, Django
+ calls :djadmin:`flush` to reset the database state. This empties all tables
+ and emits the :data:`~django.db.models.signals.post_syncdb` signal, which
+ re-creates one content type and three permissions for each model. This
+ operation gets expensive proportionally to the number of models.
+
+ Setting ``available_apps`` to a list of applications instructs Django to
+ behave as if only the models from these applications were available. The
+ behavior of ``TransactionTestCase`` changes as follows:
+
+ - :data:`~django.db.models.signals.post_syncdb` is fired before each
+ test to create the content types and permissions for each model in
+ available apps, in case they're missing.
+ - After each test, Django empties only tables corresponding to models in
+ available apps. However, at the database level, truncation may cascade to
+ related models in unavailable apps. Furthermore
+ :data:`~django.db.models.signals.post_syncdb` isn't fired; it will be
+ fired by the next ``TransactionTestCase``, after the correct set of
+ applications is selected.
+
+ Since the database isn't fully flushed, if a test creates instances of
+ models not included in ``available_apps``, they will leak and they may
+ cause unrelated tests to fail. Be careful with tests that use sessions;
+ the default session engine stores them in the database.
+
+ Since :data:`~django.db.models.signals.post_syncdb` isn't emitted after
+ flushing the database, its state after a ``TransactionTestCase`` isn't the
+ same as after a ``TestCase``: it's missing the rows created by listeners
+ to :data:`~django.db.models.signals.post_syncdb`. Considering the
+ :ref:`order in which tests are executed <order-of-tests>`, this isn't an
+ issue, provided either all ``TransactionTestCase`` in a given test suite
+ declare ``available_apps``, or none of them.
+
+ ``available_apps`` is mandatory in Django's own test suite.
+
+.. attribute:: TransactionTestCase.reset_sequences
+
+ .. versionadded:: 1.5
+
+ Setting ``reset_sequences = True`` on a ``TransactionTestCase`` will make
+ sure sequences are always reset before the test run::
+
+ class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
+ reset_sequences = True
+
+ def test_animal_pk(self):
+ lion = Animal.objects.create(name="lion", sound="roar")
+ # lion.pk is guaranteed to always be 1
+ self.assertEqual(lion.pk, 1)
+
+ Unless you are explicitly testing primary keys sequence numbers, it is
+ recommended that you do not hard code primary key values in tests.
+
+ Using ``reset_sequences = True`` will slow down the test, since the primary
+ key reset is an relatively expensive database operation.
+
Running tests outside the test runner
=====================================
diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt
index 2b1db5e501..8268051a36 100644
--- a/docs/topics/testing/overview.txt
+++ b/docs/topics/testing/overview.txt
@@ -213,6 +213,8 @@ advanced settings.
The :ref:`advanced multi-db testing topics <topics-testing-advanced-multidb>`.
+.. _order-of-tests:
+
Order in which tests are executed
---------------------------------
@@ -908,8 +910,8 @@ TransactionTestCase
.. class:: TransactionTestCase()
-Django ``TestCase`` classes make use of database transaction facilities, if
-available, to speed up the process of resetting the database to a known state
+Django's ``TestCase`` class (described below) makes use of database transaction
+facilities to speed up the process of resetting the database to a known state
at the beginning of each test. A consequence of this, however, is that the
effects of transaction commit and rollback cannot be tested by a Django
``TestCase`` class. If your test requires testing of such transactional
@@ -927,9 +929,9 @@ to test the effects of commit and rollback:
Instead, it encloses the test code in a database transaction that is rolled
back at the end of the test. Both explicit commits like
``transaction.commit()`` and implicit ones that may be caused by
- ``Model.save()`` are replaced with a ``nop`` operation. This guarantees that
- the rollback at the end of the test restores the database to its initial
- state.
+ ``transaction.atomic()`` are replaced with a ``nop`` operation. This
+ guarantees that the rollback at the end of the test restores the database to
+ its initial state.
When running on a database that does not support rollback (e.g. MySQL with the
MyISAM storage engine), ``TestCase`` falls back to initializing the database
@@ -940,22 +942,21 @@ to test the effects of commit and rollback:
While ``commit`` and ``rollback`` operations still *appear* to work when
used in ``TestCase``, no actual commit or rollback will be performed by the
database. This can cause your tests to pass or fail unexpectedly. Always
- use ``TransactionalTestCase`` when testing transactional behavior.
-
-.. note::
+ use ``TransactionTestCase`` when testing transactional behavior.
- .. versionchanged:: 1.5
+.. versionchanged:: 1.5
- Prior to 1.5, ``TransactionTestCase`` flushed the database tables *before*
- each test. In Django 1.5, this is instead done *after* the test has been run.
+ Prior to 1.5, :class:`~django.test.TransactionTestCase` flushed the
+ database tables *before* each test. In Django 1.5, this is instead done
+ *after* the test has been run.
When the flush took place before the test, it was guaranteed that primary
key values started at one in :class:`~django.test.TransactionTestCase`
tests.
- Tests should not depend on this behavior, but for legacy tests that do, the
- :attr:`~TransactionTestCase.reset_sequences` attribute can be used until
- the test has been properly updated.
+ Tests should not depend on this behavior, but for legacy tests that do,
+ the :attr:`~TransactionTestCase.reset_sequences` attribute can be used
+ until the test has been properly updated.
.. versionchanged:: 1.5
@@ -964,27 +965,6 @@ to test the effects of commit and rollback:
``TransactionTestCase`` inherits from :class:`~django.test.SimpleTestCase`.
-.. attribute:: TransactionTestCase.reset_sequences
-
- .. versionadded:: 1.5
-
- Setting ``reset_sequences = True`` on a ``TransactionTestCase`` will make
- sure sequences are always reset before the test run::
-
- class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
- reset_sequences = True
-
- def test_animal_pk(self):
- lion = Animal.objects.create(name="lion", sound="roar")
- # lion.pk is guaranteed to always be 1
- self.assertEqual(lion.pk, 1)
-
- Unless you are explicitly testing primary keys sequence numbers, it is
- recommended that you do not hard code primary key values in tests.
-
- Using ``reset_sequences = True`` will slow down the test, since the primary
- key reset is an relatively expensive database operation.
-
TestCase
~~~~~~~~
@@ -1467,7 +1447,7 @@ Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
If you use any of Django's custom ``TestCase`` classes, the test runner will
-clear thecontents of the test email outbox at the start of each test case.
+clear the contents of the test email outbox at the start of each test case.
For more detail on email services during tests, see `Email services`_ below.
diff --git a/setup.py b/setup.py
index b2b7821557..2c64868d00 100644
--- a/setup.py
+++ b/setup.py
@@ -100,8 +100,10 @@ setup(
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Topic :: Internet :: WWW/HTTP',
diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py
index 78ccf074d5..41dc0e32de 100644
--- a/tests/admin_inlines/tests.py
+++ b/tests/admin_inlines/tests.py
@@ -505,9 +505,11 @@ class TestInlinePermissions(TestCase):
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
- webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+
+ available_apps = ['admin_inlines'] + AdminSeleniumWebDriverTestCase.available_apps
fixtures = ['admin-views-users.xml']
urls = "admin_inlines.urls"
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
def test_add_stackeds(self):
"""
diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py
index 9e6b1f557b..2f399acb23 100644
--- a/tests/admin_scripts/tests.py
+++ b/tests/admin_scripts/tests.py
@@ -1192,6 +1192,21 @@ class ManageRunserver(AdminScriptTestCase):
self.cmd.handle(addrport="deadbeef:7654")
self.assertServerSettings('deadbeef', '7654')
+class ManageRunserverEmptyAllowedHosts(AdminScriptTestCase):
+ def setUp(self):
+ self.write_settings('settings.py', sdict={
+ 'ALLOWED_HOSTS': [],
+ 'DEBUG': False,
+ })
+
+ def tearDown(self):
+ self.remove_settings('settings.py')
+
+ def test_empty_allowed_hosts_error(self):
+ out, err = self.run_manage(['runserver'])
+ self.assertNoOutput(out)
+ self.assertOutput(err, 'CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.')
+
##########################################################################
# COMMAND PROCESSING TESTS
@@ -1459,6 +1474,13 @@ class ArgumentOrder(AdminScriptTestCase):
class StartProject(LiveServerTestCase, AdminScriptTestCase):
+ available_apps = [
+ 'admin_scripts',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ ]
+
def test_wrong_args(self):
"Make sure passing the wrong kinds of arguments raises a CommandError"
out, err = self.run_django_admin(['startproject'])
diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py
index 4a9a203f50..637f643261 100644
--- a/tests/admin_util/tests.py
+++ b/tests/admin_util/tests.py
@@ -301,7 +301,7 @@ class UtilTests(SimpleTestCase):
self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
'<label for="id_text" class="required inline"><i>text</i>:</label>')
self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
- '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>')
+ '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i>:</label>')
# normal strings needs to be escaped
class MyForm(forms.Form):
@@ -312,7 +312,7 @@ class UtilTests(SimpleTestCase):
self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(),
'<label for="id_text" class="required inline">&amp;text:</label>')
self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(),
- '<label for="id_cb" class="vCheckboxLabel required inline">&amp;cb</label>')
+ '<label for="id_cb" class="vCheckboxLabel required inline">&amp;cb:</label>')
def test_flatten_fieldsets(self):
"""
diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py
index 8c8a65318c..528c728069 100644
--- a/tests/admin_views/tests.py
+++ b/tests/admin_views/tests.py
@@ -22,7 +22,7 @@ from django.contrib.admin.util import quote
from django.contrib.admin.views.main import IS_POPUP_VAR
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
from django.contrib.auth import REDIRECT_FIELD_NAME
-from django.contrib.auth.models import Group, User, Permission, UNUSABLE_PASSWORD
+from django.contrib.auth.models import Group, User, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db import connection
@@ -3308,9 +3308,11 @@ class PrePopulatedTest(TestCase):
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class SeleniumAdminViewsFirefoxTests(AdminSeleniumWebDriverTestCase):
- webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
- urls = "admin_views.urls"
+
+ available_apps = ['admin_views'] + AdminSeleniumWebDriverTestCase.available_apps
fixtures = ['admin-views-users.xml']
+ urls = "admin_views.urls"
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
def test_prepopulated_fields(self):
"""
@@ -3630,7 +3632,7 @@ class UserAdminTest(TestCase):
new_user = User.objects.order_by('-id')[0]
self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
self.assertEqual(User.objects.count(), user_count + 1)
- self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+ self.assertTrue(new_user.has_usable_password())
def test_save_continue_editing_button(self):
user_count = User.objects.count()
@@ -3643,7 +3645,7 @@ class UserAdminTest(TestCase):
new_user = User.objects.order_by('-id')[0]
self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
self.assertEqual(User.objects.count(), user_count + 1)
- self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+ self.assertTrue(new_user.has_usable_password())
def test_password_mismatch(self):
response = self.client.post('/test_admin/admin/auth/user/add/', {
@@ -3689,7 +3691,7 @@ class UserAdminTest(TestCase):
new_user = User.objects.order_by('-id')[0]
self.assertRedirects(response, '/test_admin/admin/auth/user/add/')
self.assertEqual(User.objects.count(), user_count + 1)
- self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD)
+ self.assertTrue(new_user.has_usable_password())
def test_user_permission_performance(self):
u = User.objects.all()[0]
@@ -4155,3 +4157,156 @@ class AdminUserMessageTest(TestCase):
self.assertContains(response,
'<li class="extra_tag info">Test tags</li>',
html=True)
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminKeepChangeListFiltersTests(TestCase):
+ urls = "admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def get_changelist_filters_querystring(self):
+ return urlencode({
+ 'is_superuser__exact': 0,
+ 'is_staff__exact': 0,
+ })
+
+ def get_preserved_filters_querystring(self):
+ return urlencode({
+ '_changelist_filters': self.get_changelist_filters_querystring()
+ })
+
+ def get_sample_user_id(self):
+ return 104
+
+ def get_changelist_url(self):
+ return '%s?%s' % (
+ reverse('admin:auth_user_changelist'),
+ self.get_changelist_filters_querystring(),
+ )
+
+ def get_add_url(self):
+ return '%s?%s' % (
+ reverse('admin:auth_user_add'),
+ self.get_preserved_filters_querystring(),
+ )
+
+ def get_change_url(self, user_id=None):
+ if user_id is None:
+ user_id = self.get_sample_user_id()
+ return "%s?%s" % (
+ reverse('admin:auth_user_change', args=(user_id,)),
+ self.get_preserved_filters_querystring(),
+ )
+
+ def get_history_url(self, user_id=None):
+ if user_id is None:
+ user_id = self.get_sample_user_id()
+ return "%s?%s" % (
+ reverse('admin:auth_user_history', args=(user_id,)),
+ self.get_preserved_filters_querystring(),
+ )
+
+ def get_delete_url(self, user_id=None):
+ if user_id is None:
+ user_id = self.get_sample_user_id()
+ return "%s?%s" % (
+ reverse('admin:auth_user_delete', args=(user_id,)),
+ self.get_preserved_filters_querystring(),
+ )
+
+ def test_changelist_view(self):
+ response = self.client.get(self.get_changelist_url())
+ self.assertEqual(response.status_code, 200)
+
+ # Check the `change_view` link has the correct querystring.
+ detail_link = """<a href="%s">joepublic</a>""" % self.get_change_url()
+ self.assertContains(response, detail_link, count=1)
+
+ def test_change_view(self):
+ # Get the `change_view`.
+ response = self.client.get(self.get_change_url())
+ self.assertEqual(response.status_code, 200)
+
+ # Check the form action.
+ form_action = """<form enctype="multipart/form-data" action="?%s" method="post" id="user_form">""" % self.get_preserved_filters_querystring()
+ self.assertContains(response, form_action, count=1)
+
+ # Check the history link.
+ history_link = """<a href="%s" class="historylink">History</a>""" % self.get_history_url()
+ self.assertContains(response, history_link, count=1)
+
+ # Check the delete link.
+ delete_link = """<a href="%s" class="deletelink">Delete</a>""" % (self.get_delete_url())
+ self.assertContains(response, delete_link, count=1)
+
+ # Test redirect on "Save".
+ post_data = {
+ 'username': 'joepublic',
+ 'last_login_0': '2007-05-30',
+ 'last_login_1': '13:20:10',
+ 'date_joined_0': '2007-05-30',
+ 'date_joined_1': '13:20:10',
+ }
+
+ post_data['_save'] = 1
+ response = self.client.post(self.get_change_url(), data=post_data)
+ self.assertRedirects(response, self.get_changelist_url())
+ post_data.pop('_save')
+
+ # Test redirect on "Save and continue".
+ post_data['_continue'] = 1
+ response = self.client.post(self.get_change_url(), data=post_data)
+ self.assertRedirects(response, self.get_change_url())
+ post_data.pop('_continue')
+
+ # Test redirect on "Save and add new".
+ post_data['_addanother'] = 1
+ response = self.client.post(self.get_change_url(), data=post_data)
+ self.assertRedirects(response, self.get_add_url())
+ post_data.pop('_addanother')
+
+ def test_add_view(self):
+ # Get the `add_view`.
+ response = self.client.get(self.get_add_url())
+ self.assertEqual(response.status_code, 200)
+
+ # Check the form action.
+ form_action = """<form enctype="multipart/form-data" action="?%s" method="post" id="user_form">""" % self.get_preserved_filters_querystring()
+ self.assertContains(response, form_action, count=1)
+
+ # Test redirect on "Save".
+ post_data = {
+ 'username': 'dummy',
+ 'password1': 'test',
+ 'password2': 'test',
+ }
+
+ post_data['_save'] = 1
+ response = self.client.post(self.get_add_url(), data=post_data)
+ self.assertRedirects(response, self.get_change_url(User.objects.latest('pk').pk))
+ post_data.pop('_save')
+
+ # Test redirect on "Save and continue".
+ post_data['username'] = 'dummy2'
+ post_data['_continue'] = 1
+ response = self.client.post(self.get_add_url(), data=post_data)
+ self.assertRedirects(response, self.get_change_url(User.objects.latest('pk').pk))
+ post_data.pop('_continue')
+
+ # Test redirect on "Save and add new".
+ post_data['username'] = 'dummy3'
+ post_data['_addanother'] = 1
+ response = self.client.post(self.get_add_url(), data=post_data)
+ self.assertRedirects(response, self.get_add_url())
+ post_data.pop('_addanother')
+
+ def test_delete_view(self):
+ # Test redirect on "Delete".
+ response = self.client.post(self.get_delete_url(), {'post': 'yes'})
+ self.assertRedirects(response, self.get_changelist_url())
diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py
index 98f41c1490..4823883f42 100644
--- a/tests/admin_widgets/tests.py
+++ b/tests/admin_widgets/tests.py
@@ -470,9 +470,11 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class DateTimePickerSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
- webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+
+ available_apps = ['admin_widgets'] + AdminSeleniumWebDriverTestCase.available_apps
fixtures = ['admin-widgets-users.xml']
urls = "admin_widgets.urls"
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
def test_show_hide_date_time_picker_widgets(self):
"""
@@ -526,9 +528,11 @@ class DateTimePickerSeleniumIETests(DateTimePickerSeleniumFirefoxTests):
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class HorizontalVerticalFilterSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
- webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
+
+ available_apps = ['admin_widgets'] + AdminSeleniumWebDriverTestCase.available_apps
fixtures = ['admin-widgets-users.xml']
urls = "admin_widgets.urls"
+ webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
def setUp(self):
self.lisa = models.Student.objects.create(name='Lisa')
diff --git a/tests/backends/tests.py b/tests/backends/tests.py
index 08cae0f7c3..b6e3e9e36f 100644
--- a/tests/backends/tests.py
+++ b/tests/backends/tests.py
@@ -345,6 +345,8 @@ class PostgresNewConnectionTest(TestCase):
# connection would implicitly rollback and cause problems during teardown.
class ConnectionCreatedSignalTest(TransactionTestCase):
+ available_apps = []
+
# Unfortunately with sqlite3 the in-memory test database cannot be closed,
# and so it cannot be re-opened during testing.
@skipUnlessDBFeature('test_db_allows_multiple_connections')
@@ -514,6 +516,8 @@ class BackendTestCase(TestCase):
# verify if its type is django.database.db.IntegrityError.
class FkConstraintsTests(TransactionTestCase):
+ available_apps = ['backends']
+
def setUp(self):
# Create a Reporter.
self.r = models.Reporter.objects.create(first_name='John', last_name='Smith')
@@ -777,6 +781,9 @@ class MySQLPKZeroTests(TestCase):
class DBConstraintTestCase(TransactionTestCase):
+
+ available_apps = ['backends']
+
def test_can_reference_existant(self):
obj = models.Object.objects.create()
ref = models.ObjectReference.objects.create(obj=obj)
diff --git a/tests/basic/tests.py b/tests/basic/tests.py
index 6bf46cce9b..4b79c47be5 100644
--- a/tests/basic/tests.py
+++ b/tests/basic/tests.py
@@ -686,6 +686,9 @@ class ModelTest(TestCase):
class ConcurrentSaveTests(TransactionTestCase):
+
+ available_apps = ['basic']
+
@skipUnlessDBFeature('test_db_allows_multiple_connections')
def test_concurrent_delete_with_save(self):
"""
diff --git a/tests/cache/tests.py b/tests/cache/tests.py
index da80c48058..7413a4aae6 100644
--- a/tests/cache/tests.py
+++ b/tests/cache/tests.py
@@ -827,6 +827,8 @@ def custom_key_func(key, key_prefix, version):
class DBCacheTests(BaseCacheTests, TransactionTestCase):
+
+ available_apps = ['cache']
backend_name = 'django.core.cache.backends.db.DatabaseCache'
def setUp(self):
diff --git a/tests/compat_checks/__init__.py b/tests/compat_checks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/compat_checks/__init__.py
diff --git a/tests/compat_checks/models.py b/tests/compat_checks/models.py
new file mode 100644
index 0000000000..78a10abba6
--- /dev/null
+++ b/tests/compat_checks/models.py
@@ -0,0 +1 @@
+# Stubby.
diff --git a/tests/compat_checks/tests.py b/tests/compat_checks/tests.py
new file mode 100644
index 0000000000..879988c905
--- /dev/null
+++ b/tests/compat_checks/tests.py
@@ -0,0 +1,107 @@
+from django.core.compat_checks import base
+from django.core.compat_checks import django_1_6_0
+from django.core.management.commands import checksetup
+from django.core.management import call_command
+from django.test import TestCase
+
+
+class StubCheckModule(object):
+ # Has no ``run_checks`` attribute & will trigger a warning.
+ __name__ = 'StubCheckModule'
+
+
+class FakeWarnings(object):
+ def __init__(self):
+ self._warnings = []
+
+ def warn(self, message):
+ self._warnings.append(message)
+
+
+class CompatChecksTestCase(TestCase):
+ def setUp(self):
+ super(CompatChecksTestCase, self).setUp()
+
+ # We're going to override the list of checks to perform for test
+ # consistency in the future.
+ self.old_compat_checks = base.COMPAT_CHECKS
+ base.COMPAT_CHECKS = [
+ django_1_6_0,
+ ]
+
+ def tearDown(self):
+ # Restore what's supposed to be in ``COMPAT_CHECKS``.
+ base.COMPAT_CHECKS = self.old_compat_checks
+ super(CompatChecksTestCase, self).tearDown()
+
+ def test_check_test_runner_new_default(self):
+ with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
+ result = django_1_6_0.check_test_runner()
+ self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result)
+
+ def test_check_test_runner_overridden(self):
+ with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
+ self.assertEqual(django_1_6_0.check_test_runner(), None)
+
+ def test_run_checks_new_default(self):
+ with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
+ result = django_1_6_0.run_checks()
+ self.assertEqual(len(result), 1)
+ self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0])
+
+ def test_run_checks_overridden(self):
+ with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
+ self.assertEqual(len(django_1_6_0.run_checks()), 0)
+
+ def test_check_compatibility(self):
+ with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
+ result = base.check_compatibility()
+ self.assertEqual(len(result), 1)
+ self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in result[0])
+
+ with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
+ self.assertEqual(len(base.check_compatibility()), 0)
+
+ def test_check_compatibility_warning(self):
+ # First, we're patching over the ``COMPAT_CHECKS`` with a stub which
+ # will trigger the warning.
+ base.COMPAT_CHECKS = [
+ StubCheckModule(),
+ ]
+
+ # Next, we unfortunately have to patch out ``warnings``.
+ old_warnings = base.warnings
+ base.warnings = FakeWarnings()
+
+ self.assertEqual(len(base.warnings._warnings), 0)
+
+ with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
+ self.assertEqual(len(base.check_compatibility()), 0)
+
+ self.assertEqual(len(base.warnings._warnings), 1)
+ self.assertTrue("The 'StubCheckModule' module lacks a 'run_checks'" in base.warnings._warnings[0])
+
+ # Restore the ``warnings``.
+ base.warnings = old_warnings
+
+ def test_management_command(self):
+ # Again, we unfortunately have to patch out ``warnings``. Different
+ old_warnings = checksetup.warnings
+ checksetup.warnings = FakeWarnings()
+
+ self.assertEqual(len(checksetup.warnings._warnings), 0)
+
+ # Should not produce any warnings.
+ with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'):
+ call_command('checksetup')
+
+ self.assertEqual(len(checksetup.warnings._warnings), 0)
+
+ with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'):
+ call_command('checksetup')
+
+ self.assertEqual(len(checksetup.warnings._warnings), 1)
+ self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in checksetup.warnings._warnings[0])
+
+ # Restore the ``warnings``.
+ base.warnings = old_warnings
diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py
index e88c95e229..a4908b2121 100644
--- a/tests/delete_regress/tests.py
+++ b/tests/delete_regress/tests.py
@@ -16,6 +16,9 @@ from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith,
# Can't run this test under SQLite, because you can't
# get two connections to an in-memory database.
class DeleteLockingTest(TransactionTestCase):
+
+ available_apps = ['delete_regress']
+
def setUp(self):
# Create a second connection to the default database
new_connections = ConnectionHandler(settings.DATABASES)
@@ -107,6 +110,9 @@ class DeleteCascadeTests(TestCase):
class DeleteCascadeTransactionTests(TransactionTestCase):
+
+ available_apps = ['delete_regress']
+
def test_inheritance(self):
"""
Auto-created many-to-many through tables referencing a parent model are
diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py
index 406a4d79b6..6c3c559660 100644
--- a/tests/file_storage/tests.py
+++ b/tests/file_storage/tests.py
@@ -7,6 +7,10 @@ import shutil
import sys
import tempfile
import time
+try:
+ from urllib.request import urlopen
+except ImportError: # Python 2
+ from urllib2 import urlopen
import zlib
from datetime import datetime, timedelta
from io import BytesIO
@@ -22,12 +26,11 @@ from django.core.files.base import File, ContentFile
from django.core.files.images import get_image_dimensions
from django.core.files.storage import FileSystemStorage, get_storage_class
from django.core.files.uploadedfile import UploadedFile
-from django.test import SimpleTestCase
+from django.test import LiveServerTestCase, SimpleTestCase
from django.utils import six
from django.utils import unittest
from django.utils._os import upath
from django.test.utils import override_settings
-from servers.tests import LiveServerBase
try:
from django.utils.image import Image
@@ -613,10 +616,15 @@ class NoNameFileTestCase(unittest.TestCase):
def test_noname_file_get_size(self):
self.assertEqual(File(BytesIO(b'A file with no name')).size, 19)
-class FileLikeObjectTestCase(LiveServerBase):
+
+class FileLikeObjectTestCase(LiveServerTestCase):
"""
Test file-like objects (#15644).
"""
+
+ available_apps = []
+ urls = 'file_storage.urls'
+
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.storage = FileSystemStorage(location=self.temp_dir)
@@ -628,12 +636,10 @@ class FileLikeObjectTestCase(LiveServerBase):
"""
Test the File storage API with a file like object coming from urllib2.urlopen()
"""
-
- file_like_object = self.urlopen('/example_view/')
+ file_like_object = urlopen(self.live_server_url + '/')
f = File(file_like_object)
stored_filename = self.storage.save("remote_file.html", f)
- remote_file = self.urlopen('/example_view/')
-
+ remote_file = urlopen(self.live_server_url + '/')
with self.storage.open(stored_filename) as stored_file:
self.assertEqual(stored_file.read(), remote_file.read())
diff --git a/tests/file_storage/urls.py b/tests/file_storage/urls.py
new file mode 100644
index 0000000000..aefd8549e1
--- /dev/null
+++ b/tests/file_storage/urls.py
@@ -0,0 +1,7 @@
+from django.conf.urls import patterns, url
+from django.http import HttpResponse
+
+
+urlpatterns = patterns('',
+ url(r'^$', lambda req: HttpResponse('example view')),
+)
diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py
index 1b4d6eeeef..d0942afdb7 100644
--- a/tests/fixtures/tests.py
+++ b/tests/fixtures/tests.py
@@ -354,6 +354,13 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase):
+ available_apps = [
+ 'fixtures',
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth',
+ 'django.contrib.sites',
+ ]
+
@skipUnlessDBFeature('supports_forward_references')
def test_format_discovery(self):
# Load fixture 1 again, using format discovery
diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py
index e0a35e300d..dbcc721d8f 100644
--- a/tests/fixtures_model_package/tests.py
+++ b/tests/fixtures_model_package/tests.py
@@ -26,6 +26,9 @@ class SampleTestCase(TestCase):
class TestNoInitialDataLoading(TransactionTestCase):
+
+ available_apps = ['fixtures_model_package']
+
def test_syncdb(self):
transaction.set_autocommit(False)
try:
diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py
index 52526ec338..0f6ac65976 100644
--- a/tests/fixtures_regress/tests.py
+++ b/tests/fixtures_regress/tests.py
@@ -685,6 +685,12 @@ class NaturalKeyFixtureTests(TestCase):
class TestTicket11101(TransactionTestCase):
+ available_apps = [
+ 'fixtures_regress',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ ]
+
def ticket_11101(self):
management.call_command(
'loaddata',
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index 1a3fb44a66..633fde5026 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -1589,9 +1589,9 @@ class FormsTestCase(TestCase):
# Recall from above that passing the "auto_id" argument to a Form gives each
# field an "id" attribute.
t = Template('''<form action="">
-<p>{{ form.username.label_tag }}: {{ form.username }}</p>
-<p>{{ form.password1.label_tag }}: {{ form.password1 }}</p>
-<p>{{ form.password2.label_tag }}: {{ form.password2 }}</p>
+<p>{{ form.username.label_tag }} {{ form.username }}</p>
+<p>{{ form.password1.label_tag }} {{ form.password1 }}</p>
+<p>{{ form.password2.label_tag }} {{ form.password2 }}</p>
<input type="submit" />
</form>''')
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action="">
@@ -1601,18 +1601,18 @@ class FormsTestCase(TestCase):
<input type="submit" />
</form>""")
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id='id_%s')})), """<form action="">
-<p><label for="id_username">Username</label>: <input id="id_username" type="text" name="username" maxlength="10" /></p>
-<p><label for="id_password1">Password1</label>: <input type="password" name="password1" id="id_password1" /></p>
-<p><label for="id_password2">Password2</label>: <input type="password" name="password2" id="id_password2" /></p>
+<p><label for="id_username">Username:</label> <input id="id_username" type="text" name="username" maxlength="10" /></p>
+<p><label for="id_password1">Password1:</label> <input type="password" name="password1" id="id_password1" /></p>
+<p><label for="id_password2">Password2:</label> <input type="password" name="password2" id="id_password2" /></p>
<input type="submit" />
</form>""")
# User form.[field].help_text to output a field's help text. If the given field
# does not have help text, nothing will be output.
t = Template('''<form action="">
-<p>{{ form.username.label_tag }}: {{ form.username }}<br />{{ form.username.help_text }}</p>
-<p>{{ form.password1.label_tag }}: {{ form.password1 }}</p>
-<p>{{ form.password2.label_tag }}: {{ form.password2 }}</p>
+<p>{{ form.username.label_tag }} {{ form.username }}<br />{{ form.username.help_text }}</p>
+<p>{{ form.password1.label_tag }} {{ form.password1 }}</p>
+<p>{{ form.password2.label_tag }} {{ form.password2 }}</p>
<input type="submit" />
</form>''')
self.assertHTMLEqual(t.render(Context({'form': UserRegistration(auto_id=False)})), """<form action="">
@@ -1819,17 +1819,17 @@ class FormsTestCase(TestCase):
testcases = [ # (args, kwargs, expected)
# without anything: just print the <label>
- ((), {}, '<label for="id_field">Field</label>'),
+ ((), {}, '<label for="id_field">Field:</label>'),
# passing just one argument: overrides the field's label
- (('custom',), {}, '<label for="id_field">custom</label>'),
+ (('custom',), {}, '<label for="id_field">custom:</label>'),
# the overriden label is escaped
- (('custom&',), {}, '<label for="id_field">custom&amp;</label>'),
- ((mark_safe('custom&'),), {}, '<label for="id_field">custom&</label>'),
+ (('custom&',), {}, '<label for="id_field">custom&amp;:</label>'),
+ ((mark_safe('custom&'),), {}, '<label for="id_field">custom&:</label>'),
# Passing attrs to add extra attributes on the <label>
- ((), {'attrs': {'class': 'pretty'}}, '<label for="id_field" class="pretty">Field</label>')
+ ((), {'attrs': {'class': 'pretty'}}, '<label for="id_field" class="pretty">Field:</label>')
]
for args, kwargs, expected in testcases:
@@ -1844,8 +1844,8 @@ class FormsTestCase(TestCase):
field = CharField()
boundfield = SomeForm(auto_id='')['field']
- self.assertHTMLEqual(boundfield.label_tag(), 'Field')
- self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&amp;')
+ self.assertHTMLEqual(boundfield.label_tag(), 'Field:')
+ self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&amp;:')
def test_boundfield_label_tag_custom_widget_id_for_label(self):
class CustomIdForLabelTextInput(TextInput):
@@ -1861,5 +1861,12 @@ class FormsTestCase(TestCase):
empty = CharField(widget=EmptyIdForLabelTextInput)
form = SomeForm()
- self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom</label>')
- self.assertHTMLEqual(form['empty'].label_tag(), '<label>Empty</label>')
+ self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom:</label>')
+ self.assertHTMLEqual(form['empty'].label_tag(), '<label>Empty:</label>')
+
+ def test_boundfield_empty_label(self):
+ class SomeForm(Form):
+ field = CharField(label='')
+ boundfield = SomeForm()['field']
+
+ self.assertHTMLEqual(boundfield.label_tag(), '<label for="id_field"></label>')
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index 1e9e7db30c..b26017bc78 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -55,81 +55,82 @@ SplitDateTimeFormSet = formset_factory(SplitDateTimeForm)
class FormsFormsetTestCase(TestCase):
+
+ def make_choiceformset(self, formset_data=None, formset_class=ChoiceFormSet,
+ total_forms=None, initial_forms=0, max_num_forms=0, **kwargs):
+ """
+ Make a ChoiceFormset from the given formset_data.
+ The data should be given as a list of (choice, votes) tuples.
+ """
+ kwargs.setdefault('prefix', 'choices')
+ kwargs.setdefault('auto_id', False)
+
+ if formset_data is None:
+ return formset_class(**kwargs)
+
+ if total_forms is None:
+ total_forms = len(formset_data)
+
+ def prefixed(*args):
+ args = (kwargs['prefix'],) + args
+ return '-'.join(args)
+
+ data = {
+ prefixed('TOTAL_FORMS'): str(total_forms),
+ prefixed('INITIAL_FORMS'): str(initial_forms),
+ prefixed('MAX_NUM_FORMS'): str(max_num_forms),
+ }
+ for i, (choice, votes) in enumerate(formset_data):
+ data[prefixed(str(i), 'choice')] = choice
+ data[prefixed(str(i), 'votes')] = votes
+
+ return formset_class(data, **kwargs)
+
def test_basic_formset(self):
# A FormSet constructor takes the same arguments as Form. Let's create a FormSet
# for adding data. By default, it displays 1 blank form. It can display more,
# but we'll look at how to do so later.
- formset = ChoiceFormSet(auto_id=False, prefix='choices')
+ formset = self.make_choiceformset()
+
self.assertHTMLEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="1000" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
<tr><th>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""")
- # On thing to note is that there needs to be a special value in the data. This
- # value tells the FormSet how many forms were displayed so it can tell how
- # many forms it needs to clean and validate. You could use javascript to create
- # new forms on the client side, but they won't get validated unless you increment
- # the TOTAL_FORMS field appropriately.
-
- data = {
- 'choices-TOTAL_FORMS': '1', # the number of forms rendered
- 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': 'Calexico',
- 'choices-0-votes': '100',
- }
# We treat FormSet pretty much like we would treat a normal Form. FormSet has an
# is_valid method, and a cleaned_data or errors attribute depending on whether all
# the forms passed validation. However, unlike a Form instance, cleaned_data and
# errors will be a list of dicts rather than just a single dict.
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset([('Calexico', '100')])
self.assertTrue(formset.is_valid())
self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}])
# If a FormSet was not passed any data, its is_valid and has_changed
# methods should return False.
- formset = ChoiceFormSet()
+ formset = self.make_choiceformset()
self.assertFalse(formset.is_valid())
self.assertFalse(formset.has_changed())
def test_formset_validation(self):
# FormSet instances can also have an error attribute if validation failed for
# any of the forms.
-
- data = {
- 'choices-TOTAL_FORMS': '1', # the number of forms rendered
- 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': 'Calexico',
- 'choices-0-votes': '',
- }
-
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset([('Calexico', '')])
self.assertFalse(formset.is_valid())
self.assertEqual(formset.errors, [{'votes': ['This field is required.']}])
def test_formset_has_changed(self):
# FormSet instances has_changed method will be True if any data is
# passed to his forms, even if the formset didn't validate
- data = {
- 'choices-TOTAL_FORMS': '1', # the number of forms rendered
- 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': '',
- 'choices-0-votes': '',
- }
- blank_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ blank_formset = self.make_choiceformset([('', '')])
self.assertFalse(blank_formset.has_changed())
# invalid formset test
- data['choices-0-choice'] = 'Calexico'
- invalid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ invalid_formset = self.make_choiceformset([('Calexico', '')])
self.assertFalse(invalid_formset.is_valid())
self.assertTrue(invalid_formset.has_changed())
# valid formset test
- data['choices-0-votes'] = '100'
- valid_formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ valid_formset = self.make_choiceformset([('Calexico', '100')])
self.assertTrue(valid_formset.is_valid())
self.assertTrue(valid_formset.has_changed())
@@ -139,7 +140,7 @@ class FormsFormsetTestCase(TestCase):
# an extra blank form is included.
initial = [{'choice': 'Calexico', 'votes': 100}]
- formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset(initial=initial)
form_output = []
for form in formset.forms:
@@ -151,18 +152,7 @@ class FormsFormsetTestCase(TestCase):
<li>Votes: <input type="number" name="choices-1-votes" /></li>""")
# Let's simulate what would happen if we submitted this form.
-
- data = {
- 'choices-TOTAL_FORMS': '2', # the number of forms rendered
- 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': 'Calexico',
- 'choices-0-votes': '100',
- 'choices-1-choice': '',
- 'choices-1-votes': '',
- }
-
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset([('Calexico', '100'), ('', '')], initial_forms=1)
self.assertTrue(formset.is_valid())
self.assertEqual([form.cleaned_data for form in formset.forms], [{'votes': 100, 'choice': 'Calexico'}, {}])
@@ -172,18 +162,7 @@ class FormsFormsetTestCase(TestCase):
# one of the fields of a blank form though, it will be validated. We may want to
# required that at least x number of forms are completed, but we'll show how to
# handle that later.
-
- data = {
- 'choices-TOTAL_FORMS': '2', # the number of forms rendered
- 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': 'Calexico',
- 'choices-0-votes': '100',
- 'choices-1-choice': 'The Decemberists',
- 'choices-1-votes': '', # missing value
- }
-
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset([('Calexico', '100'), ('The Decemberists', '')], initial_forms=1)
self.assertFalse(formset.is_valid())
self.assertEqual(formset.errors, [{}, {'votes': ['This field is required.']}])
@@ -191,18 +170,7 @@ class FormsFormsetTestCase(TestCase):
# If we delete data that was pre-filled, we should get an error. Simply removing
# data from form fields isn't the proper way to delete it. We'll see how to
# handle that case later.
-
- data = {
- 'choices-TOTAL_FORMS': '2', # the number of forms rendered
- 'choices-INITIAL_FORMS': '1', # the number of forms with initial data
- 'choices-MAX_NUM_FORMS': '0', # max number of forms
- 'choices-0-choice': '', # deleted value
- 'choices-0-votes': '', # deleted value
- 'choices-1-choice': '',
- 'choices-1-votes': '',
- }
-
- formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ formset = self.make_choiceformset([('', ''), ('', '')], initial_forms=1)
self.assertFalse(formset.is_valid())
self.assertEqual(formset.errors, [{'votes': ['This field is required.'], 'choice': ['This field is required.']}, {}])
@@ -1027,6 +995,40 @@ class FormsFormsetTestCase(TestCase):
self.assertTrue(formset.is_valid())
+ def test_formset_total_error_count(self):
+ """A valid formset should have 0 total errors."""
+ data = [ # formset_data, expected error count
+ ([('Calexico', '100')], 0),
+ ([('Calexico', '')], 1),
+ ([('', 'invalid')], 2),
+ ([('Calexico', '100'), ('Calexico', '')], 1),
+ ([('Calexico', ''), ('Calexico', '')], 2),
+ ]
+
+ for formset_data, expected_error_count in data:
+ formset = self.make_choiceformset(formset_data)
+ self.assertEqual(formset.total_error_count(), expected_error_count)
+
+ def test_formset_total_error_count_with_non_form_errors(self):
+ data = {
+ 'choices-TOTAL_FORMS': '2', # the number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # the number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '2', # max number of forms - should be ignored
+ 'choices-0-choice': 'Zero',
+ 'choices-0-votes': '0',
+ 'choices-1-choice': 'One',
+ 'choices-1-votes': '1',
+ }
+
+ ChoiceFormSet = formset_factory(Choice, extra=1, max_num=1, validate_max=True)
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertEqual(formset.total_error_count(), 1)
+
+ data['choices-1-votes'] = ''
+ formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertEqual(formset.total_error_count(), 2)
+
+
data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered
'choices-INITIAL_FORMS': '0', # the number of forms with initial data
@@ -1087,7 +1089,7 @@ class TestIsBoundBehavior(TestCase):
self.assertEqual([{}], formset.cleaned_data)
- def test_form_errors_are_cought_by_formset(self):
+ def test_form_errors_are_caught_by_formset(self):
data = {
'form-TOTAL_FORMS': '2',
'form-INITIAL_FORMS': '0',
diff --git a/tests/forms_tests/tests/test_regressions.py b/tests/forms_tests/tests/test_regressions.py
index ea138d32d5..d230d68f75 100644
--- a/tests/forms_tests/tests/test_regressions.py
+++ b/tests/forms_tests/tests/test_regressions.py
@@ -45,8 +45,8 @@ class FormsRegressionsTestCase(TransRealMixin, TestCase):
field_2 = CharField(max_length=10, label=ugettext_lazy('field_2'), widget=TextInput(attrs={'id': 'field_2_id'}))
f = SomeForm()
- self.assertHTMLEqual(f['field_1'].label_tag(), '<label for="id_field_1">field_1</label>')
- self.assertHTMLEqual(f['field_2'].label_tag(), '<label for="field_2_id">field_2</label>')
+ self.assertHTMLEqual(f['field_1'].label_tag(), '<label for="id_field_1">field_1:</label>')
+ self.assertHTMLEqual(f['field_2'].label_tag(), '<label for="field_2_id">field_2:</label>')
# Unicode decoding problems...
GENDERS = (('\xc5', 'En tied\xe4'), ('\xf8', 'Mies'), ('\xdf', 'Nainen'))
diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py
index 4664553aa7..4c566dc8e4 100644
--- a/tests/forms_tests/tests/test_widgets.py
+++ b/tests/forms_tests/tests/test_widgets.py
@@ -1028,6 +1028,8 @@ class WidgetTests(TestCase):
class LiveWidgetTests(AdminSeleniumWebDriverTestCase):
+
+ available_apps = ['forms_tests'] + AdminSeleniumWebDriverTestCase.available_apps
urls = 'forms_tests.urls'
def test_textarea_trailing_newlines(self):
diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py
index 2616ddaf7d..99a67c320c 100644
--- a/tests/forms_tests/tests/tests.py
+++ b/tests/forms_tests/tests/tests.py
@@ -208,7 +208,8 @@ class RelatedModelFormTests(TestCase):
ref = models.ForeignKey("B")
class Meta:
- model=A
+ model = A
+ fields = '__all__'
self.assertRaises(ValueError, ModelFormMetaclass, str('Form'), (ModelForm,), {'Meta': Meta})
@@ -226,7 +227,8 @@ class RelatedModelFormTests(TestCase):
pass
class Meta:
- model=A
+ model = A
+ fields = '__all__'
self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm))
diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py
index ffd9b1b480..f95732f8b5 100644
--- a/tests/generic_views/test_base.py
+++ b/tests/generic_views/test_base.py
@@ -317,7 +317,9 @@ class TemplateViewTest(TestCase):
self.assertEqual(response['Content-Type'], 'text/plain')
-class RedirectViewTest(unittest.TestCase):
+class RedirectViewTest(TestCase):
+ urls = 'generic_views.urls'
+
rf = RequestFactory()
def test_no_url(self):
@@ -360,6 +362,22 @@ class RedirectViewTest(unittest.TestCase):
self.assertEqual(response.status_code, 301)
self.assertEqual(response.url, '/bar/42/')
+ def test_named_url_pattern(self):
+ "Named pattern parameter should reverse to the matching pattern"
+ response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), pk=1)
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response['Location'], '/detail/artist/1/')
+
+ def test_named_url_pattern_using_args(self):
+ response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), 1)
+ self.assertEqual(response.status_code, 301)
+ self.assertEqual(response['Location'], '/detail/artist/1/')
+
+ def test_wrong_named_url_pattern(self):
+ "A wrong pattern name returns 410 GONE"
+ response = RedirectView.as_view(pattern_name='wrong.pattern_name')(self.rf.get('/foo/'))
+ self.assertEqual(response.status_code, 410)
+
def test_redirect_POST(self):
"Default is a permanent redirect"
response = RedirectView.as_view(url='/bar/')(self.rf.post('/foo/'))
diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py
index 36c248b169..847a6dec01 100644
--- a/tests/get_or_create/tests.py
+++ b/tests/get_or_create/tests.py
@@ -94,6 +94,8 @@ class GetOrCreateTests(TestCase):
class GetOrCreateTransactionTests(TransactionTestCase):
+ available_apps = ['get_or_create']
+
def test_get_or_create_integrityerror(self):
# Regression test for #15117. Requires a TransactionTestCase on
# databases that delay integrity checks until the end of transactions,
diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py
index 397b63647a..2f9f304b81 100644
--- a/tests/handlers/tests.py
+++ b/tests/handlers/tests.py
@@ -37,6 +37,8 @@ class HandlerTests(TestCase):
class TransactionsPerRequestTests(TransactionTestCase):
+
+ available_apps = []
urls = 'handlers.urls'
def test_no_transaction(self):
diff --git a/tests/lookup/models.py b/tests/lookup/models.py
index f60c4962fd..f388ddf403 100644
--- a/tests/lookup/models.py
+++ b/tests/lookup/models.py
@@ -10,16 +10,29 @@ from django.db import models
from django.utils import six
from django.utils.encoding import python_2_unicode_compatible
-from shared_models.models import Author, Book
+class Author(models.Model):
+ name = models.CharField(max_length=100)
+ class Meta:
+ ordering = ('name', )
+
+@python_2_unicode_compatible
+class Article(models.Model):
+ headline = models.CharField(max_length=100)
+ pub_date = models.DateTimeField()
+ author = models.ForeignKey(Author, blank=True, null=True)
+ class Meta:
+ ordering = ('-pub_date', 'headline')
+
+ def __str__(self):
+ return self.headline
class Tag(models.Model):
- articles = models.ManyToManyField(Book)
+ articles = models.ManyToManyField(Article)
name = models.CharField(max_length=100)
class Meta:
ordering = ('name', )
-
@python_2_unicode_compatible
class Season(models.Model):
year = models.PositiveSmallIntegerField()
@@ -28,7 +41,6 @@ class Season(models.Model):
def __str__(self):
return six.text_type(self.year)
-
@python_2_unicode_compatible
class Game(models.Model):
season = models.ForeignKey(Season, related_name='games')
@@ -38,7 +50,6 @@ class Game(models.Model):
def __str__(self):
return "%s at %s" % (self.away, self.home)
-
@python_2_unicode_compatible
class Player(models.Model):
name = models.CharField(max_length=100)
diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py
index 6a6729ef22..de7105f92d 100644
--- a/tests/lookup/tests.py
+++ b/tests/lookup/tests.py
@@ -5,11 +5,8 @@ from operator import attrgetter
from django.core.exceptions import FieldError
from django.test import TestCase, skipUnlessDBFeature
-from django.utils import six
-from shared_models.models import Author, Book
-
-from .models import Tag, Game, Season, Player
+from .models import Author, Article, Tag, Game, Season, Player
class LookupTests(TestCase):
@@ -20,165 +17,165 @@ class LookupTests(TestCase):
self.au1.save()
self.au2 = Author(name='Author 2')
self.au2.save()
- # Create a couple of Books.
- self.b1 = Book(title='Book 1', pubdate=datetime(2005, 7, 26), author=self.au1)
- self.b1.save()
- self.b2 = Book(title='Book 2', pubdate=datetime(2005, 7, 27), author=self.au1)
- self.b2.save()
- self.b3 = Book(title='Book 3', pubdate=datetime(2005, 7, 27), author=self.au1)
- self.b3.save()
- self.b4 = Book(title='Book 4', pubdate=datetime(2005, 7, 28), author=self.au1)
- self.b4.save()
- self.b5 = Book(title='Book 5', pubdate=datetime(2005, 8, 1, 9, 0), author=self.au2)
- self.b5.save()
- self.b6 = Book(title='Book 6', pubdate=datetime(2005, 8, 1, 8, 0), author=self.au2)
- self.b6.save()
- self.b7 = Book(title='Book 7', pubdate=datetime(2005, 7, 27), author=self.au2)
- self.b7.save()
+ # Create a couple of Articles.
+ self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26), author=self.au1)
+ self.a1.save()
+ self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27), author=self.au1)
+ self.a2.save()
+ self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27), author=self.au1)
+ self.a3.save()
+ self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28), author=self.au1)
+ self.a4.save()
+ self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0), author=self.au2)
+ self.a5.save()
+ self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0), author=self.au2)
+ self.a6.save()
+ self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27), author=self.au2)
+ self.a7.save()
# Create a few Tags.
self.t1 = Tag(name='Tag 1')
self.t1.save()
- self.t1.articles.add(self.b1, self.b2, self.b3)
+ self.t1.articles.add(self.a1, self.a2, self.a3)
self.t2 = Tag(name='Tag 2')
self.t2.save()
- self.t2.articles.add(self.b3, self.b4, self.b5)
+ self.t2.articles.add(self.a3, self.a4, self.a5)
self.t3 = Tag(name='Tag 3')
self.t3.save()
- self.t3.articles.add(self.b5, self.b6, self.b7)
+ self.t3.articles.add(self.a5, self.a6, self.a7)
def test_exists(self):
# We can use .exists() to check that there are some
- self.assertTrue(Book.objects.exists())
- for a in Book.objects.all():
+ self.assertTrue(Article.objects.exists())
+ for a in Article.objects.all():
a.delete()
# There should be none now!
- self.assertFalse(Book.objects.exists())
+ self.assertFalse(Article.objects.exists())
def test_lookup_int_as_str(self):
# Integer value can be queried using string
- self.assertQuerysetEqual(Book.objects.filter(id__iexact=str(self.b1.id)),
- ['<Book: Book 1>'])
+ self.assertQuerysetEqual(Article.objects.filter(id__iexact=str(self.a1.id)),
+ ['<Article: Article 1>'])
@skipUnlessDBFeature('supports_date_lookup_using_string')
def test_lookup_date_as_str(self):
# A date lookup can be performed using a string search
- self.assertQuerysetEqual(Book.objects.filter(pubdate__startswith='2005'),
+ self.assertQuerysetEqual(Article.objects.filter(pub_date__startswith='2005'),
[
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
def test_iterator(self):
# Each QuerySet gets iterator(), which is a generator that "lazily"
# returns results using database-level iteration.
- self.assertQuerysetEqual(Book.objects.iterator(),
+ self.assertQuerysetEqual(Article.objects.iterator(),
[
- 'Book 5',
- 'Book 6',
- 'Book 4',
- 'Book 2',
- 'Book 3',
- 'Book 7',
- 'Book 1',
+ 'Article 5',
+ 'Article 6',
+ 'Article 4',
+ 'Article 2',
+ 'Article 3',
+ 'Article 7',
+ 'Article 1',
],
- transform=attrgetter('title'))
+ transform=attrgetter('headline'))
# iterator() can be used on any QuerySet.
self.assertQuerysetEqual(
- Book.objects.filter(title__endswith='4').iterator(),
- ['Book 4'],
- transform=attrgetter('title'))
+ Article.objects.filter(headline__endswith='4').iterator(),
+ ['Article 4'],
+ transform=attrgetter('headline'))
def test_count(self):
# count() returns the number of objects matching search criteria.
- self.assertEqual(Book.objects.count(), 7)
- self.assertEqual(Book.objects.filter(pubdate__exact=datetime(2005, 7, 27)).count(), 3)
- self.assertEqual(Book.objects.filter(title__startswith='Blah blah').count(), 0)
+ self.assertEqual(Article.objects.count(), 7)
+ self.assertEqual(Article.objects.filter(pub_date__exact=datetime(2005, 7, 27)).count(), 3)
+ self.assertEqual(Article.objects.filter(headline__startswith='Blah blah').count(), 0)
# count() should respect sliced query sets.
- articles = Book.objects.all()
+ articles = Article.objects.all()
self.assertEqual(articles.count(), 7)
self.assertEqual(articles[:4].count(), 4)
self.assertEqual(articles[1:100].count(), 6)
self.assertEqual(articles[10:100].count(), 0)
# Date and date/time lookups can also be done with strings.
- self.assertEqual(Book.objects.filter(pubdate__exact='2005-07-27').count(), 3)
+ self.assertEqual(Article.objects.filter(pub_date__exact='2005-07-27 00:00:00').count(), 3)
def test_in_bulk(self):
# in_bulk() takes a list of IDs and returns a dictionary mapping IDs to objects.
- arts = Book.objects.in_bulk([self.b1.id, self.b2.id])
- self.assertEqual(arts[self.b1.id], self.b1)
- self.assertEqual(arts[self.b2.id], self.b2)
- self.assertEqual(Book.objects.in_bulk([self.b3.id]), {self.b3.id: self.b3})
- self.assertEqual(Book.objects.in_bulk(set([self.b3.id])), {self.b3.id: self.b3})
- self.assertEqual(Book.objects.in_bulk(frozenset([self.b3.id])), {self.b3.id: self.b3})
- self.assertEqual(Book.objects.in_bulk((self.b3.id,)), {self.b3.id: self.b3})
- self.assertEqual(Book.objects.in_bulk([1000]), {})
- self.assertEqual(Book.objects.in_bulk([]), {})
- self.assertEqual(Book.objects.in_bulk(iter([self.b1.id])), {self.b1.id: self.b1})
- self.assertEqual(Book.objects.in_bulk(iter([])), {})
- self.assertRaises(TypeError, Book.objects.in_bulk)
- self.assertRaises(TypeError, Book.objects.in_bulk, name__startswith='Blah')
+ arts = Article.objects.in_bulk([self.a1.id, self.a2.id])
+ self.assertEqual(arts[self.a1.id], self.a1)
+ self.assertEqual(arts[self.a2.id], self.a2)
+ self.assertEqual(Article.objects.in_bulk([self.a3.id]), {self.a3.id: self.a3})
+ self.assertEqual(Article.objects.in_bulk(set([self.a3.id])), {self.a3.id: self.a3})
+ self.assertEqual(Article.objects.in_bulk(frozenset([self.a3.id])), {self.a3.id: self.a3})
+ self.assertEqual(Article.objects.in_bulk((self.a3.id,)), {self.a3.id: self.a3})
+ self.assertEqual(Article.objects.in_bulk([1000]), {})
+ self.assertEqual(Article.objects.in_bulk([]), {})
+ self.assertEqual(Article.objects.in_bulk(iter([self.a1.id])), {self.a1.id: self.a1})
+ self.assertEqual(Article.objects.in_bulk(iter([])), {})
+ self.assertRaises(TypeError, Article.objects.in_bulk)
+ self.assertRaises(TypeError, Article.objects.in_bulk, headline__startswith='Blah')
def test_values(self):
# values() returns a list of dictionaries instead of object instances --
# and you can specify which fields you want to retrieve.
identity = lambda x:x
- self.assertQuerysetEqual(Book.objects.values('title'),
+ self.assertQuerysetEqual(Article.objects.values('headline'),
[
- {'title': 'Book 5'},
- {'title': 'Book 6'},
- {'title': 'Book 4'},
- {'title': 'Book 2'},
- {'title': 'Book 3'},
- {'title': 'Book 7'},
- {'title': 'Book 1'},
+ {'headline': 'Article 5'},
+ {'headline': 'Article 6'},
+ {'headline': 'Article 4'},
+ {'headline': 'Article 2'},
+ {'headline': 'Article 3'},
+ {'headline': 'Article 7'},
+ {'headline': 'Article 1'},
],
transform=identity)
self.assertQuerysetEqual(
- Book.objects.filter(pubdate__exact=datetime(2005, 7, 27)).values('id'),
- [{'id': self.b2.id}, {'id': self.b3.id}, {'id': self.b7.id}],
+ Article.objects.filter(pub_date__exact=datetime(2005, 7, 27)).values('id'),
+ [{'id': self.a2.id}, {'id': self.a3.id}, {'id': self.a7.id}],
transform=identity)
- self.assertQuerysetEqual(Book.objects.values('id', 'title'),
+ self.assertQuerysetEqual(Article.objects.values('id', 'headline'),
[
- {'id': self.b5.id, 'title': 'Book 5'},
- {'id': self.b6.id, 'title': 'Book 6'},
- {'id': self.b4.id, 'title': 'Book 4'},
- {'id': self.b2.id, 'title': 'Book 2'},
- {'id': self.b3.id, 'title': 'Book 3'},
- {'id': self.b7.id, 'title': 'Book 7'},
- {'id': self.b1.id, 'title': 'Book 1'},
+ {'id': self.a5.id, 'headline': 'Article 5'},
+ {'id': self.a6.id, 'headline': 'Article 6'},
+ {'id': self.a4.id, 'headline': 'Article 4'},
+ {'id': self.a2.id, 'headline': 'Article 2'},
+ {'id': self.a3.id, 'headline': 'Article 3'},
+ {'id': self.a7.id, 'headline': 'Article 7'},
+ {'id': self.a1.id, 'headline': 'Article 1'},
],
transform=identity)
# You can use values() with iterator() for memory savings,
# because iterator() uses database-level iteration.
- self.assertQuerysetEqual(Book.objects.values('id', 'title').iterator(),
+ self.assertQuerysetEqual(Article.objects.values('id', 'headline').iterator(),
[
- {'title': 'Book 5', 'id': self.b5.id},
- {'title': 'Book 6', 'id': self.b6.id},
- {'title': 'Book 4', 'id': self.b4.id},
- {'title': 'Book 2', 'id': self.b2.id},
- {'title': 'Book 3', 'id': self.b3.id},
- {'title': 'Book 7', 'id': self.b7.id},
- {'title': 'Book 1', 'id': self.b1.id},
+ {'headline': 'Article 5', 'id': self.a5.id},
+ {'headline': 'Article 6', 'id': self.a6.id},
+ {'headline': 'Article 4', 'id': self.a4.id},
+ {'headline': 'Article 2', 'id': self.a2.id},
+ {'headline': 'Article 3', 'id': self.a3.id},
+ {'headline': 'Article 7', 'id': self.a7.id},
+ {'headline': 'Article 1', 'id': self.a1.id},
],
transform=identity)
# The values() method works with "extra" fields specified in extra(select).
self.assertQuerysetEqual(
- Book.objects.extra(select={'id_plus_one': 'id + 1'}).values('id', 'id_plus_one'),
+ Article.objects.extra(select={'id_plus_one': 'id + 1'}).values('id', 'id_plus_one'),
[
- {'id': self.b5.id, 'id_plus_one': self.b5.id + 1},
- {'id': self.b6.id, 'id_plus_one': self.b6.id + 1},
- {'id': self.b4.id, 'id_plus_one': self.b4.id + 1},
- {'id': self.b2.id, 'id_plus_one': self.b2.id + 1},
- {'id': self.b3.id, 'id_plus_one': self.b3.id + 1},
- {'id': self.b7.id, 'id_plus_one': self.b7.id + 1},
- {'id': self.b1.id, 'id_plus_one': self.b1.id + 1},
+ {'id': self.a5.id, 'id_plus_one': self.a5.id + 1},
+ {'id': self.a6.id, 'id_plus_one': self.a6.id + 1},
+ {'id': self.a4.id, 'id_plus_one': self.a4.id + 1},
+ {'id': self.a2.id, 'id_plus_one': self.a2.id + 1},
+ {'id': self.a3.id, 'id_plus_one': self.a3.id + 1},
+ {'id': self.a7.id, 'id_plus_one': self.a7.id + 1},
+ {'id': self.a1.id, 'id_plus_one': self.a1.id + 1},
],
transform=identity)
data = {
@@ -192,67 +189,66 @@ class LookupTests(TestCase):
'id_plus_eight': 'id+8',
}
self.assertQuerysetEqual(
- Book.objects.filter(id=self.b1.id).extra(select=data).values(*data.keys()),
+ Article.objects.filter(id=self.a1.id).extra(select=data).values(*data.keys()),
[{
- 'id_plus_one': self.b1.id + 1,
- 'id_plus_two': self.b1.id + 2,
- 'id_plus_three': self.b1.id + 3,
- 'id_plus_four': self.b1.id + 4,
- 'id_plus_five': self.b1.id + 5,
- 'id_plus_six': self.b1.id + 6,
- 'id_plus_seven': self.b1.id + 7,
- 'id_plus_eight': self.b1.id + 8,
+ 'id_plus_one': self.a1.id + 1,
+ 'id_plus_two': self.a1.id + 2,
+ 'id_plus_three': self.a1.id + 3,
+ 'id_plus_four': self.a1.id + 4,
+ 'id_plus_five': self.a1.id + 5,
+ 'id_plus_six': self.a1.id + 6,
+ 'id_plus_seven': self.a1.id + 7,
+ 'id_plus_eight': self.a1.id + 8,
}], transform=identity)
# You can specify fields from forward and reverse relations, just like filter().
self.assertQuerysetEqual(
- Book.objects.values('title', 'author__name'),
+ Article.objects.values('headline', 'author__name'),
[
- {'title': self.b5.title, 'author__name': self.au2.name},
- {'title': self.b6.title, 'author__name': self.au2.name},
- {'title': self.b4.title, 'author__name': self.au1.name},
- {'title': self.b2.title, 'author__name': self.au1.name},
- {'title': self.b3.title, 'author__name': self.au1.name},
- {'title': self.b7.title, 'author__name': self.au2.name},
- {'title': self.b1.title, 'author__name': self.au1.name},
+ {'headline': self.a5.headline, 'author__name': self.au2.name},
+ {'headline': self.a6.headline, 'author__name': self.au2.name},
+ {'headline': self.a4.headline, 'author__name': self.au1.name},
+ {'headline': self.a2.headline, 'author__name': self.au1.name},
+ {'headline': self.a3.headline, 'author__name': self.au1.name},
+ {'headline': self.a7.headline, 'author__name': self.au2.name},
+ {'headline': self.a1.headline, 'author__name': self.au1.name},
], transform=identity)
self.assertQuerysetEqual(
- Author.objects.values('name', 'book__title').order_by('name', 'book__title'),
+ Author.objects.values('name', 'article__headline').order_by('name', 'article__headline'),
[
- {'name': self.au1.name, 'book__title': self.b1.title},
- {'name': self.au1.name, 'book__title': self.b2.title},
- {'name': self.au1.name, 'book__title': self.b3.title},
- {'name': self.au1.name, 'book__title': self.b4.title},
- {'name': self.au2.name, 'book__title': self.b5.title},
- {'name': self.au2.name, 'book__title': self.b6.title},
- {'name': self.au2.name, 'book__title': self.b7.title},
+ {'name': self.au1.name, 'article__headline': self.a1.headline},
+ {'name': self.au1.name, 'article__headline': self.a2.headline},
+ {'name': self.au1.name, 'article__headline': self.a3.headline},
+ {'name': self.au1.name, 'article__headline': self.a4.headline},
+ {'name': self.au2.name, 'article__headline': self.a5.headline},
+ {'name': self.au2.name, 'article__headline': self.a6.headline},
+ {'name': self.au2.name, 'article__headline': self.a7.headline},
], transform=identity)
self.assertQuerysetEqual(
- Author.objects.values('name', 'book__title', 'book__tag__name').order_by('name', 'book__title', 'book__tag__name'),
+ Author.objects.values('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
[
- {'name': self.au1.name, 'book__title': self.b1.title, 'book__tag__name': self.t1.name},
- {'name': self.au1.name, 'book__title': self.b2.title, 'book__tag__name': self.t1.name},
- {'name': self.au1.name, 'book__title': self.b3.title, 'book__tag__name': self.t1.name},
- {'name': self.au1.name, 'book__title': self.b3.title, 'book__tag__name': self.t2.name},
- {'name': self.au1.name, 'book__title': self.b4.title, 'book__tag__name': self.t2.name},
- {'name': self.au2.name, 'book__title': self.b5.title, 'book__tag__name': self.t2.name},
- {'name': self.au2.name, 'book__title': self.b5.title, 'book__tag__name': self.t3.name},
- {'name': self.au2.name, 'book__title': self.b6.title, 'book__tag__name': self.t3.name},
- {'name': self.au2.name, 'book__title': self.b7.title, 'book__tag__name': self.t3.name},
+ {'name': self.au1.name, 'article__headline': self.a1.headline, 'article__tag__name': self.t1.name},
+ {'name': self.au1.name, 'article__headline': self.a2.headline, 'article__tag__name': self.t1.name},
+ {'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t1.name},
+ {'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t2.name},
+ {'name': self.au1.name, 'article__headline': self.a4.headline, 'article__tag__name': self.t2.name},
+ {'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t2.name},
+ {'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t3.name},
+ {'name': self.au2.name, 'article__headline': self.a6.headline, 'article__tag__name': self.t3.name},
+ {'name': self.au2.name, 'article__headline': self.a7.headline, 'article__tag__name': self.t3.name},
], transform=identity)
# However, an exception FieldDoesNotExist will be thrown if you specify
# a non-existent field name in values() (a field that is neither in the
# model nor in extra(select)).
self.assertRaises(FieldError,
- Book.objects.extra(select={'id_plus_one': 'id + 1'}).values,
+ Article.objects.extra(select={'id_plus_one': 'id + 1'}).values,
'id', 'id_plus_two')
# If you don't specify field names to values(), all are returned.
- self.assertQuerysetEqual(Book.objects.filter(id=self.b5.id).values(),
+ self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(),
[{
- 'id': self.b5.id,
+ 'id': self.a5.id,
'author_id': self.au2.id,
- 'title': 'Book 5',
- 'pages': 0,
- 'pubdate': datetime(2005, 8, 1, 9, 0)
+ 'headline': 'Article 5',
+ 'pub_date': datetime(2005, 8, 1, 9, 0)
}], transform=identity)
def test_values_list(self):
@@ -261,353 +257,358 @@ class LookupTests(TestCase):
# Within each tuple, the order of the elements is the same as the order
# of fields in the values_list() call.
identity = lambda x:x
- self.assertQuerysetEqual(Book.objects.values_list('title'),
+ self.assertQuerysetEqual(Article.objects.values_list('headline'),
[
- ('Book 5',),
- ('Book 6',),
- ('Book 4',),
- ('Book 2',),
- ('Book 3',),
- ('Book 7',),
- ('Book 1',),
+ ('Article 5',),
+ ('Article 6',),
+ ('Article 4',),
+ ('Article 2',),
+ ('Article 3',),
+ ('Article 7',),
+ ('Article 1',),
], transform=identity)
- self.assertQuerysetEqual(Book.objects.values_list('id').order_by('id'),
- [(self.b1.id,), (self.b2.id,), (self.b3.id,), (self.b4.id,), (self.b5.id,), (self.b6.id,), (self.b7.id,)],
+ self.assertQuerysetEqual(Article.objects.values_list('id').order_by('id'),
+ [(self.a1.id,), (self.a2.id,), (self.a3.id,), (self.a4.id,), (self.a5.id,), (self.a6.id,), (self.a7.id,)],
transform=identity)
self.assertQuerysetEqual(
- Book.objects.values_list('id', flat=True).order_by('id'),
- [self.b1.id, self.b2.id, self.b3.id, self.b4.id, self.b5.id, self.b6.id, self.b7.id],
+ Article.objects.values_list('id', flat=True).order_by('id'),
+ [self.a1.id, self.a2.id, self.a3.id, self.a4.id, self.a5.id, self.a6.id, self.a7.id],
transform=identity)
self.assertQuerysetEqual(
- Book.objects.extra(select={'id_plus_one': 'id+1'})
+ Article.objects.extra(select={'id_plus_one': 'id+1'})
.order_by('id').values_list('id'),
- [(self.b1.id,), (self.b2.id,), (self.b3.id,), (self.b4.id,), (self.b5.id,), (self.b6.id,), (self.b7.id,)],
+ [(self.a1.id,), (self.a2.id,), (self.a3.id,), (self.a4.id,), (self.a5.id,), (self.a6.id,), (self.a7.id,)],
transform=identity)
self.assertQuerysetEqual(
- Book.objects.extra(select={'id_plus_one': 'id+1'})
+ Article.objects.extra(select={'id_plus_one': 'id+1'})
.order_by('id').values_list('id_plus_one', 'id'),
[
- (self.b1.id+1, self.b1.id),
- (self.b2.id+1, self.b2.id),
- (self.b3.id+1, self.b3.id),
- (self.b4.id+1, self.b4.id),
- (self.b5.id+1, self.b5.id),
- (self.b6.id+1, self.b6.id),
- (self.b7.id+1, self.b7.id)
+ (self.a1.id+1, self.a1.id),
+ (self.a2.id+1, self.a2.id),
+ (self.a3.id+1, self.a3.id),
+ (self.a4.id+1, self.a4.id),
+ (self.a5.id+1, self.a5.id),
+ (self.a6.id+1, self.a6.id),
+ (self.a7.id+1, self.a7.id)
],
transform=identity)
self.assertQuerysetEqual(
- Book.objects.extra(select={'id_plus_one': 'id+1'})
+ Article.objects.extra(select={'id_plus_one': 'id+1'})
.order_by('id').values_list('id', 'id_plus_one'),
[
- (self.b1.id, self.b1.id+1),
- (self.b2.id, self.b2.id+1),
- (self.b3.id, self.b3.id+1),
- (self.b4.id, self.b4.id+1),
- (self.b5.id, self.b5.id+1),
- (self.b6.id, self.b6.id+1),
- (self.b7.id, self.b7.id+1)
+ (self.a1.id, self.a1.id+1),
+ (self.a2.id, self.a2.id+1),
+ (self.a3.id, self.a3.id+1),
+ (self.a4.id, self.a4.id+1),
+ (self.a5.id, self.a5.id+1),
+ (self.a6.id, self.a6.id+1),
+ (self.a7.id, self.a7.id+1)
],
transform=identity)
self.assertQuerysetEqual(
- Author.objects.values_list('name', 'book__title', 'book__tag__name').order_by('name', 'book__title', 'book__tag__name'),
+ Author.objects.values_list('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
[
- (self.au1.name, self.b1.title, self.t1.name),
- (self.au1.name, self.b2.title, self.t1.name),
- (self.au1.name, self.b3.title, self.t1.name),
- (self.au1.name, self.b3.title, self.t2.name),
- (self.au1.name, self.b4.title, self.t2.name),
- (self.au2.name, self.b5.title, self.t2.name),
- (self.au2.name, self.b5.title, self.t3.name),
- (self.au2.name, self.b6.title, self.t3.name),
- (self.au2.name, self.b7.title, self.t3.name),
+ (self.au1.name, self.a1.headline, self.t1.name),
+ (self.au1.name, self.a2.headline, self.t1.name),
+ (self.au1.name, self.a3.headline, self.t1.name),
+ (self.au1.name, self.a3.headline, self.t2.name),
+ (self.au1.name, self.a4.headline, self.t2.name),
+ (self.au2.name, self.a5.headline, self.t2.name),
+ (self.au2.name, self.a5.headline, self.t3.name),
+ (self.au2.name, self.a6.headline, self.t3.name),
+ (self.au2.name, self.a7.headline, self.t3.name),
], transform=identity)
- self.assertRaises(TypeError, Book.objects.values_list, 'id', 'title', flat=True)
+ self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True)
def test_get_next_previous_by(self):
# Every DateField and DateTimeField creates get_next_by_FOO() and
# get_previous_by_FOO() methods. In the case of identical date values,
# these methods will use the ID as a fallback check. This guarantees
# that no records are skipped or duplicated.
- self.assertEqual(repr(self.b1.get_next_by_pubdate()),
- '<Book: Book 2>')
- self.assertEqual(repr(self.b2.get_next_by_pubdate()),
- '<Book: Book 3>')
- self.assertEqual(repr(self.b2.get_next_by_pubdate(title__endswith='6')),
- '<Book: Book 6>')
- self.assertEqual(repr(self.b3.get_next_by_pubdate()),
- '<Book: Book 7>')
- self.assertEqual(repr(self.b4.get_next_by_pubdate()),
- '<Book: Book 6>')
- self.assertRaises(Book.DoesNotExist, self.b5.get_next_by_pubdate)
- self.assertEqual(repr(self.b6.get_next_by_pubdate()),
- '<Book: Book 5>')
- self.assertEqual(repr(self.b7.get_next_by_pubdate()),
- '<Book: Book 4>')
+ self.assertEqual(repr(self.a1.get_next_by_pub_date()),
+ '<Article: Article 2>')
+ self.assertEqual(repr(self.a2.get_next_by_pub_date()),
+ '<Article: Article 3>')
+ self.assertEqual(repr(self.a2.get_next_by_pub_date(headline__endswith='6')),
+ '<Article: Article 6>')
+ self.assertEqual(repr(self.a3.get_next_by_pub_date()),
+ '<Article: Article 7>')
+ self.assertEqual(repr(self.a4.get_next_by_pub_date()),
+ '<Article: Article 6>')
+ self.assertRaises(Article.DoesNotExist, self.a5.get_next_by_pub_date)
+ self.assertEqual(repr(self.a6.get_next_by_pub_date()),
+ '<Article: Article 5>')
+ self.assertEqual(repr(self.a7.get_next_by_pub_date()),
+ '<Article: Article 4>')
- self.assertEqual(repr(self.b7.get_previous_by_pubdate()),
- '<Book: Book 3>')
- self.assertEqual(repr(self.b6.get_previous_by_pubdate()),
- '<Book: Book 4>')
- self.assertEqual(repr(self.b5.get_previous_by_pubdate()),
- '<Book: Book 6>')
- self.assertEqual(repr(self.b4.get_previous_by_pubdate()),
- '<Book: Book 7>')
- self.assertEqual(repr(self.b3.get_previous_by_pubdate()),
- '<Book: Book 2>')
- self.assertEqual(repr(self.b2.get_previous_by_pubdate()),
- '<Book: Book 1>')
+ self.assertEqual(repr(self.a7.get_previous_by_pub_date()),
+ '<Article: Article 3>')
+ self.assertEqual(repr(self.a6.get_previous_by_pub_date()),
+ '<Article: Article 4>')
+ self.assertEqual(repr(self.a5.get_previous_by_pub_date()),
+ '<Article: Article 6>')
+ self.assertEqual(repr(self.a4.get_previous_by_pub_date()),
+ '<Article: Article 7>')
+ self.assertEqual(repr(self.a3.get_previous_by_pub_date()),
+ '<Article: Article 2>')
+ self.assertEqual(repr(self.a2.get_previous_by_pub_date()),
+ '<Article: Article 1>')
def test_escaping(self):
# Underscores, percent signs and backslashes have special meaning in the
# underlying SQL code, but Django handles the quoting of them automatically.
- b8 = Book(title='Book_ with underscore', pubdate=datetime(2005, 11, 20))
- b8.save()
- self.assertQuerysetEqual(Book.objects.filter(title__startswith='Book'),
+ a8 = Article(headline='Article_ with underscore', pub_date=datetime(2005, 11, 20))
+ a8.save()
+ self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article'),
[
- '<Book: Book_ with underscore>',
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article_ with underscore>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
- self.assertQuerysetEqual(Book.objects.filter(title__startswith='Book_'),
- ['<Book: Book_ with underscore>'])
- b9 = Book(title='Book% with percent sign', pubdate=datetime(2005, 11, 21))
- b9.save()
- self.assertQuerysetEqual(Book.objects.filter(title__startswith='Book'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article_'),
+ ['<Article: Article_ with underscore>'])
+ a9 = Article(headline='Article% with percent sign', pub_date=datetime(2005, 11, 21))
+ a9.save()
+ self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article'),
[
- '<Book: Book% with percent sign>',
- '<Book: Book_ with underscore>',
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article% with percent sign>',
+ '<Article: Article_ with underscore>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
- self.assertQuerysetEqual(Book.objects.filter(title__startswith='Book%'),
- ['<Book: Book% with percent sign>'])
- b10 = Book(title='Book with \\ backslash', pubdate=datetime(2005, 11, 22))
- b10.save()
- self.assertQuerysetEqual(Book.objects.filter(title__contains='\\'),
- ['<Book: Book with \ backslash>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__startswith='Article%'),
+ ['<Article: Article% with percent sign>'])
+ a10 = Article(headline='Article with \\ backslash', pub_date=datetime(2005, 11, 22))
+ a10.save()
+ self.assertQuerysetEqual(Article.objects.filter(headline__contains='\\'),
+ ['<Article: Article with \ backslash>'])
def test_exclude(self):
- b8 = Book.objects.create(title='Book_ with underscore', pubdate=datetime(2005, 11, 20))
- b9 = Book.objects.create(title='Book% with percent sign', pubdate=datetime(2005, 11, 21))
- b10 = Book.objects.create(title='Book with \\ backslash', pubdate=datetime(2005, 11, 22))
+ a8 = Article.objects.create(headline='Article_ with underscore', pub_date=datetime(2005, 11, 20))
+ a9 = Article.objects.create(headline='Article% with percent sign', pub_date=datetime(2005, 11, 21))
+ a10 = Article.objects.create(headline='Article with \\ backslash', pub_date=datetime(2005, 11, 22))
# exclude() is the opposite of filter() when doing lookups:
self.assertQuerysetEqual(
- Book.objects.filter(title__contains='Book').exclude(title__contains='with'),
+ Article.objects.filter(headline__contains='Article').exclude(headline__contains='with'),
[
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
- self.assertQuerysetEqual(Book.objects.exclude(title__startswith="Book_"),
+ self.assertQuerysetEqual(Article.objects.exclude(headline__startswith="Article_"),
[
- '<Book: Book with \\ backslash>',
- '<Book: Book% with percent sign>',
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article with \\ backslash>',
+ '<Article: Article% with percent sign>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
- self.assertQuerysetEqual(Book.objects.exclude(title="Book 7"),
+ self.assertQuerysetEqual(Article.objects.exclude(headline="Article 7"),
[
- '<Book: Book with \\ backslash>',
- '<Book: Book% with percent sign>',
- '<Book: Book_ with underscore>',
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 1>',
+ '<Article: Article with \\ backslash>',
+ '<Article: Article% with percent sign>',
+ '<Article: Article_ with underscore>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 1>',
])
def test_none(self):
# none() returns a QuerySet that behaves like any other QuerySet object
- self.assertQuerysetEqual(Book.objects.none(), [])
+ self.assertQuerysetEqual(Article.objects.none(), [])
self.assertQuerysetEqual(
- Book.objects.none().filter(title__startswith='Book'), [])
+ Article.objects.none().filter(headline__startswith='Article'), [])
self.assertQuerysetEqual(
- Book.objects.filter(title__startswith='Book').none(), [])
- self.assertEqual(Book.objects.none().count(), 0)
+ Article.objects.filter(headline__startswith='Article').none(), [])
+ self.assertEqual(Article.objects.none().count(), 0)
self.assertEqual(
- Book.objects.none().update(title="This should not take effect"), 0)
+ Article.objects.none().update(headline="This should not take effect"), 0)
self.assertQuerysetEqual(
- [article for article in Book.objects.none().iterator()],
+ [article for article in Article.objects.none().iterator()],
[])
def test_in(self):
# using __in with an empty list should return an empty query set
- self.assertQuerysetEqual(Book.objects.filter(id__in=[]), [])
- self.assertQuerysetEqual(Book.objects.exclude(id__in=[]),
+ self.assertQuerysetEqual(Article.objects.filter(id__in=[]), [])
+ self.assertQuerysetEqual(Article.objects.exclude(id__in=[]),
[
- '<Book: Book 5>',
- '<Book: Book 6>',
- '<Book: Book 4>',
- '<Book: Book 2>',
- '<Book: Book 3>',
- '<Book: Book 7>',
- '<Book: Book 1>',
+ '<Article: Article 5>',
+ '<Article: Article 6>',
+ '<Article: Article 4>',
+ '<Article: Article 2>',
+ '<Article: Article 3>',
+ '<Article: Article 7>',
+ '<Article: Article 1>',
])
def test_error_messages(self):
# Programming errors are pointed out with nice error messages
- with six.assertRaisesRegex(self, FieldError, "Cannot resolve keyword 'pubdate_year' "
- "into field. Choices are: .+"):
- Book.objects.filter(pubdate_year='2005').count()
-
- with self.assertRaises(FieldError, msg="Join on field 'title' not permitted. "
- "Did you misspell 'starts' for the lookup type?"):
- Book.objects.filter(title__starts='Book')
+ try:
+ Article.objects.filter(pub_date_year='2005').count()
+ self.fail('FieldError not raised')
+ except FieldError as ex:
+ self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' "
+ "into field. Choices are: author, headline, id, pub_date, tag")
+ try:
+ Article.objects.filter(headline__starts='Article')
+ self.fail('FieldError not raised')
+ except FieldError as ex:
+ self.assertEqual(str(ex), "Join on field 'headline' not permitted. "
+ "Did you misspell 'starts' for the lookup type?")
def test_regex(self):
- # Create some articles with a bit more interesting names for testing field lookups:
- for a in Book.objects.all():
+ # Create some articles with a bit more interesting headlines for testing field lookups:
+ for a in Article.objects.all():
a.delete()
now = datetime.now()
- b1 = Book(pubdate=now, title='f')
- b1.save()
- b2 = Book(pubdate=now, title='fo')
- b2.save()
- b3 = Book(pubdate=now, title='foo')
- b3.save()
- b4 = Book(pubdate=now, title='fooo')
- b4.save()
- b5 = Book(pubdate=now, title='hey-Foo')
- b5.save()
- b6 = Book(pubdate=now, title='bar')
- b6.save()
- b7 = Book(pubdate=now, title='AbBa')
- b7.save()
- b8 = Book(pubdate=now, title='baz')
- b8.save()
- b9 = Book(pubdate=now, title='baxZ')
- b9.save()
+ a1 = Article(pub_date=now, headline='f')
+ a1.save()
+ a2 = Article(pub_date=now, headline='fo')
+ a2.save()
+ a3 = Article(pub_date=now, headline='foo')
+ a3.save()
+ a4 = Article(pub_date=now, headline='fooo')
+ a4.save()
+ a5 = Article(pub_date=now, headline='hey-Foo')
+ a5.save()
+ a6 = Article(pub_date=now, headline='bar')
+ a6.save()
+ a7 = Article(pub_date=now, headline='AbBa')
+ a7.save()
+ a8 = Article(pub_date=now, headline='baz')
+ a8.save()
+ a9 = Article(pub_date=now, headline='baxZ')
+ a9.save()
# zero-or-more
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'fo*'),
- ['<Book: f>', '<Book: fo>', '<Book: foo>', '<Book: fooo>'])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'fo*'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fo*'),
+ ['<Article: f>', '<Article: fo>', '<Article: foo>', '<Article: fooo>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'fo*'),
[
- '<Book: f>',
- '<Book: fo>',
- '<Book: foo>',
- '<Book: fooo>',
- '<Book: hey-Foo>',
+ '<Article: f>',
+ '<Article: fo>',
+ '<Article: foo>',
+ '<Article: fooo>',
+ '<Article: hey-Foo>',
])
# one-or-more
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'fo+'),
- ['<Book: fo>', '<Book: foo>', '<Book: fooo>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fo+'),
+ ['<Article: fo>', '<Article: foo>', '<Article: fooo>'])
# wildcard
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'fooo?'),
- ['<Book: foo>', '<Book: fooo>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'fooo?'),
+ ['<Article: foo>', '<Article: fooo>'])
# leading anchor
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'^b'),
- ['<Book: bar>', '<Book: baxZ>', '<Book: baz>'])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'^a'),
- ['<Book: AbBa>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'^b'),
+ ['<Article: bar>', '<Article: baxZ>', '<Article: baz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'^a'),
+ ['<Article: AbBa>'])
# trailing anchor
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'z$'),
- ['<Book: baz>'])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'z$'),
- ['<Book: baxZ>', '<Book: baz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'z$'),
+ ['<Article: baz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'z$'),
+ ['<Article: baxZ>', '<Article: baz>'])
# character sets
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'ba[rz]'),
- ['<Book: bar>', '<Book: baz>'])
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'ba.[RxZ]'),
- ['<Book: baxZ>'])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'ba[RxZ]'),
- ['<Book: bar>', '<Book: baxZ>', '<Book: baz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'ba[rz]'),
+ ['<Article: bar>', '<Article: baz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'ba.[RxZ]'),
+ ['<Article: baxZ>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'ba[RxZ]'),
+ ['<Article: bar>', '<Article: baxZ>', '<Article: baz>'])
# and more articles:
- b10 = Book(pubdate=now, title='foobar')
- b10.save()
- b11 = Book(pubdate=now, title='foobaz')
- b11.save()
- b12 = Book(pubdate=now, title='ooF')
- b12.save()
- b13 = Book(pubdate=now, title='foobarbaz')
- b13.save()
- b14 = Book(pubdate=now, title='zoocarfaz')
- b14.save()
- b15 = Book(pubdate=now, title='barfoobaz')
- b15.save()
- b16 = Book(pubdate=now, title='bazbaRFOO')
- b16.save()
+ a10 = Article(pub_date=now, headline='foobar')
+ a10.save()
+ a11 = Article(pub_date=now, headline='foobaz')
+ a11.save()
+ a12 = Article(pub_date=now, headline='ooF')
+ a12.save()
+ a13 = Article(pub_date=now, headline='foobarbaz')
+ a13.save()
+ a14 = Article(pub_date=now, headline='zoocarfaz')
+ a14.save()
+ a15 = Article(pub_date=now, headline='barfoobaz')
+ a15.save()
+ a16 = Article(pub_date=now, headline='bazbaRFOO')
+ a16.save()
# alternation
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'oo(f|b)'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'oo(f|b)'),
[
- '<Book: barfoobaz>',
- '<Book: foobar>',
- '<Book: foobarbaz>',
- '<Book: foobaz>',
+ '<Article: barfoobaz>',
+ '<Article: foobar>',
+ '<Article: foobarbaz>',
+ '<Article: foobaz>',
])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'oo(f|b)'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'oo(f|b)'),
[
- '<Book: barfoobaz>',
- '<Book: foobar>',
- '<Book: foobarbaz>',
- '<Book: foobaz>',
- '<Book: ooF>',
+ '<Article: barfoobaz>',
+ '<Article: foobar>',
+ '<Article: foobarbaz>',
+ '<Article: foobaz>',
+ '<Article: ooF>',
])
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'^foo(f|b)'),
- ['<Book: foobar>', '<Book: foobarbaz>', '<Book: foobaz>'])
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'^foo(f|b)'),
+ ['<Article: foobar>', '<Article: foobarbaz>', '<Article: foobaz>'])
# greedy matching
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'b.*az'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b.*az'),
[
- '<Book: barfoobaz>',
- '<Book: baz>',
- '<Book: bazbaRFOO>',
- '<Book: foobarbaz>',
- '<Book: foobaz>',
+ '<Article: barfoobaz>',
+ '<Article: baz>',
+ '<Article: bazbaRFOO>',
+ '<Article: foobarbaz>',
+ '<Article: foobaz>',
])
- self.assertQuerysetEqual(Book.objects.filter(title__iregex=r'b.*ar'),
+ self.assertQuerysetEqual(Article.objects.filter(headline__iregex=r'b.*ar'),
[
- '<Book: bar>',
- '<Book: barfoobaz>',
- '<Book: bazbaRFOO>',
- '<Book: foobar>',
- '<Book: foobarbaz>',
+ '<Article: bar>',
+ '<Article: barfoobaz>',
+ '<Article: bazbaRFOO>',
+ '<Article: foobar>',
+ '<Article: foobarbaz>',
])
@skipUnlessDBFeature('supports_regex_backreferencing')
def test_regex_backreferencing(self):
# grouping and backreferences
now = datetime.now()
- b10 = Book(pubdate=now, title='foobar')
- b10.save()
- b11 = Book(pubdate=now, title='foobaz')
- b11.save()
- b12 = Book(pubdate=now, title='ooF')
- b12.save()
- b13 = Book(pubdate=now, title='foobarbaz')
- b13.save()
- b14 = Book(pubdate=now, title='zoocarfaz')
- b14.save()
- b15 = Book(pubdate=now, title='barfoobaz')
- b15.save()
- b16 = Book(pubdate=now, title='bazbaRFOO')
- b16.save()
- self.assertQuerysetEqual(Book.objects.filter(title__regex=r'b(.).*b\1'),
- ['<Book: barfoobaz>', '<Book: bazbaRFOO>', '<Book: foobarbaz>'])
+ a10 = Article(pub_date=now, headline='foobar')
+ a10.save()
+ a11 = Article(pub_date=now, headline='foobaz')
+ a11.save()
+ a12 = Article(pub_date=now, headline='ooF')
+ a12.save()
+ a13 = Article(pub_date=now, headline='foobarbaz')
+ a13.save()
+ a14 = Article(pub_date=now, headline='zoocarfaz')
+ a14.save()
+ a15 = Article(pub_date=now, headline='barfoobaz')
+ a15.save()
+ a16 = Article(pub_date=now, headline='bazbaRFOO')
+ a16.save()
+ self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'),
+ ['<Article: barfoobaz>', '<Article: bazbaRFOO>', '<Article: foobarbaz>'])
def test_nonfield_lookups(self):
"""
@@ -615,11 +616,11 @@ class LookupTests(TestCase):
exception.
"""
with self.assertRaises(FieldError):
- Book.objects.filter(name__blahblah=99)
+ Article.objects.filter(headline__blahblah=99)
with self.assertRaises(FieldError):
- Book.objects.filter(name__blahblah__exact=99)
+ Article.objects.filter(headline__blahblah__exact=99)
with self.assertRaises(FieldError):
- Book.objects.filter(blahblah=99)
+ Article.objects.filter(blahblah=99)
def test_lookup_collision(self):
"""
diff --git a/tests/m2m_through_regress/tests.py b/tests/m2m_through_regress/tests.py
index 5ac10462fa..de4d52a2db 100644
--- a/tests/m2m_through_regress/tests.py
+++ b/tests/m2m_through_regress/tests.py
@@ -10,6 +10,7 @@ from .models import (Person, Group, Membership, UserMembership, Car, Driver,
class M2MThroughTestCase(TestCase):
+
def test_everything(self):
bob = Person.objects.create(name="Bob")
jim = Person.objects.create(name="Jim")
diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py
index 265eb97c36..20645cf91f 100644
--- a/tests/middleware/tests.py
+++ b/tests/middleware/tests.py
@@ -707,6 +707,9 @@ class TransactionMiddlewareTest(IgnorePendingDeprecationWarningsMixin, Transacti
"""
Test the transaction middleware.
"""
+
+ available_apps = ['middleware']
+
def setUp(self):
super(TransactionMiddlewareTest, self).setUp()
self.request = HttpRequest()
diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py
index 610dc34001..a4cf9471de 100644
--- a/tests/model_forms/models.py
+++ b/tests/model_forms/models.py
@@ -11,13 +11,13 @@ from __future__ import unicode_literals
import os
import tempfile
+from django.core import validators
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import FileSystemStorage
from django.db import models
from django.utils import six
from django.utils.encoding import python_2_unicode_compatible
-from shared_models.models import Author, Book
temp_storage_dir = tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR'])
temp_storage = FileSystemStorage(temp_storage_dir)
@@ -47,12 +47,22 @@ class Category(models.Model):
return self.__str__()
@python_2_unicode_compatible
+class Writer(models.Model):
+ name = models.CharField(max_length=50, help_text='Use both first and last names.')
+
+ class Meta:
+ ordering = ('name',)
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
class Article(models.Model):
headline = models.CharField(max_length=50)
slug = models.SlugField()
pub_date = models.DateField()
created = models.DateField(editable=False)
- writer = models.ForeignKey(Author)
+ writer = models.ForeignKey(Writer)
article = models.TextField()
categories = models.ManyToManyField(Category, blank=True)
status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True)
@@ -72,12 +82,12 @@ class ImprovedArticle(models.Model):
class ImprovedArticleWithParentLink(models.Model):
article = models.OneToOneField(Article, parent_link=True)
-class BetterAuthor(Author):
+class BetterWriter(Writer):
score = models.IntegerField()
@python_2_unicode_compatible
-class AuthorProfile(models.Model):
- writer = models.OneToOneField(Author, primary_key=True)
+class WriterProfile(models.Model):
+ writer = models.OneToOneField(Writer, primary_key=True)
age = models.PositiveIntegerField()
def __str__(self):
@@ -177,6 +187,14 @@ class Inventory(models.Model):
def __repr__(self):
return self.__str__()
+class Book(models.Model):
+ title = models.CharField(max_length=40)
+ author = models.ForeignKey(Writer, blank=True, null=True)
+ special_id = models.IntegerField(blank=True, null=True, unique=True)
+
+ class Meta:
+ unique_together = ('title', 'author')
+
class BookXtra(models.Model):
isbn = models.CharField(max_length=16, unique=True)
suffix1 = models.IntegerField(blank=True, default=0)
@@ -269,3 +287,12 @@ class ColourfulItem(models.Model):
class ArticleStatusNote(models.Model):
name = models.CharField(max_length=20)
status = models.ManyToManyField(ArticleStatus)
+
+class CustomErrorMessage(models.Model):
+ name1 = models.CharField(max_length=50,
+ validators=[validators.validate_slug],
+ error_messages={'invalid': 'Model custom error message.'})
+
+ name2 = models.CharField(max_length=50,
+ validators=[validators.validate_slug],
+ error_messages={'invalid': 'Model custom error message.'})
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index 58dde13a8a..39be824798 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -17,14 +17,12 @@ from django.utils.unittest import skipUnless
from django.test import TestCase
from django.utils import six
-from shared_models.models import Author, Book
-
-from .models import (Article, ArticleStatus, BetterAuthor, BigInt,
+from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book,
Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook,
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
ImprovedArticleWithParentLink, Inventory, Post, Price,
- Product, TextFile, AuthorProfile, Colour, ColourfulItem,
- ArticleStatusNote, DateTimePost, test_images)
+ Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem,
+ ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images)
if test_images:
from .models import ImageFile, OptionalImageFile
@@ -54,14 +52,15 @@ class PriceForm(forms.ModelForm):
class BookForm(forms.ModelForm):
class Meta:
- fields = ['title', 'author', 'pubdate']
model = Book
+ fields = '__all__'
class DerivedBookForm(forms.ModelForm):
class Meta:
- fields = ['title', 'author', 'isbn', 'suffix1', 'suffix2']
model = DerivedBook
+ fields = '__all__'
+
class ExplicitPKForm(forms.ModelForm):
@@ -88,11 +87,11 @@ class DerivedPostForm(forms.ModelForm):
fields = '__all__'
-class CustomAuthorForm(forms.ModelForm):
+class CustomWriterForm(forms.ModelForm):
name = forms.CharField(required=False)
class Meta:
- model = Author
+ model = Writer
fields = '__all__'
@@ -128,7 +127,7 @@ class PartialArticleForm(forms.ModelForm):
class RoykoForm(forms.ModelForm):
class Meta:
- model = Author
+ model = Writer
fields = '__all__'
@@ -188,15 +187,14 @@ class ImprovedArticleWithParentLinkForm(forms.ModelForm):
fields = '__all__'
-class BetterAuthorForm(forms.ModelForm):
+class BetterWriterForm(forms.ModelForm):
class Meta:
- model = BetterAuthor
+ model = BetterWriter
fields = '__all__'
-
-class AuthorProfileForm(forms.ModelForm):
+class WriterProfileForm(forms.ModelForm):
class Meta:
- model = AuthorProfile
+ model = WriterProfile
fields = '__all__'
@@ -254,6 +252,12 @@ class StatusNoteCBM2mForm(forms.ModelForm):
fields = '__all__'
widgets = {'status': forms.CheckboxSelectMultiple}
+class CustomErrorMessageForm(forms.ModelForm):
+ name1 = forms.CharField(error_messages={'invalid': 'Form custom error message.'})
+
+ class Meta:
+ model = CustomErrorMessage
+
class ModelFormBaseTest(TestCase):
def test_base_form(self):
@@ -324,14 +328,14 @@ class ModelFormBaseTest(TestCase):
forms.fields.BooleanField)
def test_override_field(self):
- class AuthorForm(forms.ModelForm):
+ class WriterForm(forms.ModelForm):
book = forms.CharField(required=False)
class Meta:
- model = Author
+ model = Writer
fields = '__all__'
- wf = AuthorForm({'name': 'Richard Lockridge'})
+ wf = WriterForm({'name': 'Richard Lockridge'})
self.assertTrue(wf.is_valid())
def test_limit_nonexistent_field(self):
@@ -495,7 +499,7 @@ class ModelFormBaseTest(TestCase):
['slug', 'name'])
-class TestWidgetForm(forms.ModelForm):
+class FieldOverridesTroughFormMetaForm(forms.ModelForm):
class Meta:
model = Category
fields = ['name', 'url', 'slug']
@@ -503,25 +507,74 @@ class TestWidgetForm(forms.ModelForm):
'name': forms.Textarea,
'url': forms.TextInput(attrs={'class': 'url'})
}
+ labels = {
+ 'name': 'Title',
+ }
+ help_texts = {
+ 'slug': 'Watch out! Letters, numbers, underscores and hyphens only.',
+ }
+ error_messages = {
+ 'slug': {
+ 'invalid': (
+ "Didn't you read the help text? "
+ "We said letters, numbers, underscores and hyphens only!"
+ )
+ }
+ }
+class TestFieldOverridesTroughFormMeta(TestCase):
+ def test_widget_overrides(self):
+ form = FieldOverridesTroughFormMetaForm()
+ self.assertHTMLEqual(
+ str(form['name']),
+ '<textarea id="id_name" rows="10" cols="40" name="name"></textarea>',
+ )
+ self.assertHTMLEqual(
+ str(form['url']),
+ '<input id="id_url" type="text" class="url" name="url" maxlength="40" />',
+ )
+ self.assertHTMLEqual(
+ str(form['slug']),
+ '<input id="id_slug" type="text" name="slug" maxlength="20" />',
+ )
-class TestWidgets(TestCase):
- def test_base_widgets(self):
- frm = TestWidgetForm()
+ def test_label_overrides(self):
+ form = FieldOverridesTroughFormMetaForm()
self.assertHTMLEqual(
- str(frm['name']),
- '<textarea id="id_name" rows="10" cols="40" name="name"></textarea>'
+ str(form['name'].label_tag()),
+ '<label for="id_name">Title:</label>',
)
self.assertHTMLEqual(
- str(frm['url']),
- '<input id="id_url" type="text" class="url" name="url" maxlength="40" />'
+ str(form['url'].label_tag()),
+ '<label for="id_url">The URL:</label>',
)
self.assertHTMLEqual(
- str(frm['slug']),
- '<input id="id_slug" type="text" name="slug" maxlength="20" />'
+ str(form['slug'].label_tag()),
+ '<label for="id_slug">Slug:</label>',
+ )
+
+ def test_help_text_overrides(self):
+ form = FieldOverridesTroughFormMetaForm()
+ self.assertEqual(
+ form['slug'].help_text,
+ 'Watch out! Letters, numbers, underscores and hyphens only.',
)
+ def test_error_messages_overrides(self):
+ form = FieldOverridesTroughFormMetaForm(data={
+ 'name': 'Category',
+ 'url': '/category/',
+ 'slug': '!%#*@',
+ })
+ form.full_clean()
+
+ error = [
+ "Didn't you read the help text? "
+ "We said letters, numbers, underscores and hyphens only!",
+ ]
+ self.assertEqual(form.errors, {'slug': error})
+
class IncompleteCategoryFormWithFields(forms.ModelForm):
"""
@@ -556,7 +609,7 @@ class ValidationTest(TestCase):
assert form.is_valid()
def test_notrequired_overrides_notblank(self):
- form = CustomAuthorForm({})
+ form = CustomWriterForm({})
assert form.is_valid()
@@ -565,7 +618,7 @@ class ValidationTest(TestCase):
# unique/unique_together validation
class UniqueTest(TestCase):
def setUp(self):
- self.author = Author.objects.create(name='Mike Royko')
+ self.writer = Writer.objects.create(name='Mike Royko')
def test_simple_unique(self):
form = ProductForm({'slug': 'teddy-bear-blue'})
@@ -589,31 +642,33 @@ class UniqueTest(TestCase):
def test_unique_null(self):
title = 'I May Be Wrong But I Doubt It'
- form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'})
+ form = BookForm({'title': title, 'author': self.writer.pk})
self.assertTrue(form.is_valid())
form.save()
- form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'})
+ form = BookForm({'title': title, 'author': self.writer.pk})
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['__all__'], ['Book with this Title and Author already exists.'])
- form = BookForm({'title': title, 'pubdate': '2012-12-12 00:00:00'})
+ form = BookForm({'title': title})
self.assertTrue(form.is_valid())
form.save()
- form = BookForm({'title': title, 'pubdate': '2012-12-12 00:00:00'})
+ form = BookForm({'title': title})
self.assertTrue(form.is_valid())
def test_inherited_unique(self):
- form = BetterAuthorForm({'name': 'Mike Royko', 'score': 3})
+ title = 'Boss'
+ Book.objects.create(title=title, author=self.writer, special_id=1)
+ form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'special_id': '1', 'isbn': '12345'})
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
- self.assertEqual(form.errors['name'], ['Author with this Name already exists.'])
+ self.assertEqual(form.errors['special_id'], ['Book with this Special id already exists.'])
def test_inherited_unique_together(self):
title = 'Boss'
- form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'})
+ form = BookForm({'title': title, 'author': self.writer.pk})
self.assertTrue(form.is_valid())
form.save()
- form = DerivedBookForm({'title': title, 'author': self.author.pk, 'isbn': '12345'})
+ form = DerivedBookForm({'title': title, 'author': self.writer.pk, 'isbn': '12345'})
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['__all__'], ['Book with this Title and Author already exists.'])
@@ -621,9 +676,8 @@ class UniqueTest(TestCase):
def test_abstract_inherited_unique(self):
title = 'Boss'
isbn = '12345'
- dbook = DerivedBook.objects.create(title=title, author=self.author, isbn=isbn,
- pubdate='2012-12-12 00:00')
- form = DerivedBookForm({'title': 'Other', 'author': self.author.pk, 'isbn': isbn})
+ dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn)
+ form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': isbn})
self.assertFalse(form.is_valid())
self.assertEqual(len(form.errors), 1)
self.assertEqual(form.errors['isbn'], ['Derived book with this Isbn already exists.'])
@@ -631,11 +685,10 @@ class UniqueTest(TestCase):
def test_abstract_inherited_unique_together(self):
title = 'Boss'
isbn = '12345'
- dbook = DerivedBook.objects.create(title=title, author=self.author, isbn=isbn,
- pubdate='2012-12-12 00:00')
+ dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn)
form = DerivedBookForm({
'title': 'Other',
- 'author': self.author.pk,
+ 'author': self.writer.pk,
'isbn': '9876',
'suffix1': '0',
'suffix2': '0'
@@ -753,7 +806,7 @@ class ModelToDictTests(TestCase):
]
for c in categories:
c.save()
- writer = Author(name='Test writer')
+ writer = Writer(name='Test writer')
writer.save()
art = Article(
@@ -862,10 +915,10 @@ class OldFormForXTests(TestCase):
with self.assertRaises(ValueError):
f.save()
- # Create a couple of Authors.
- w_royko = Author(name='Mike Royko')
+ # Create a couple of Writers.
+ w_royko = Writer(name='Mike Royko')
w_royko.save()
- w_woodward = Author(name='Bob Woodward')
+ w_woodward = Writer(name='Bob Woodward')
w_woodward.save()
# ManyToManyFields are represented by a MultipleChoiceField, ForeignKeys and any
# fields with the 'choices' attribute are represented by a ChoiceField.
@@ -903,9 +956,9 @@ class OldFormForXTests(TestCase):
# When the ModelForm is passed an instance, that instance's current values are
# inserted as 'initial' data in each Field.
- w = Author.objects.get(name='Mike Royko')
+ w = Writer.objects.get(name='Mike Royko')
f = RoykoForm(auto_id=False, instance=w)
- self.assertHTMLEqual(six.text_type(f), '''<tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="100" /><br /><span class="helptext">Use both first and last names.</span></td></tr>''')
+ self.assertHTMLEqual(six.text_type(f), '''<tr><th>Name:</th><td><input type="text" name="name" value="Mike Royko" maxlength="50" /><br /><span class="helptext">Use both first and last names.</span></td></tr>''')
art = Article(
headline='Test article',
@@ -1117,7 +1170,7 @@ class OldFormForXTests(TestCase):
c4 = Category.objects.create(name='Fourth', url='4th')
self.assertEqual(c4.name, 'Fourth')
- w_bernstein = Author.objects.create(name='Carl Bernstein')
+ w_bernstein = Writer.objects.create(name='Carl Bernstein')
self.assertEqual(w_bernstein.name, 'Carl Bernstein')
self.assertHTMLEqual(f.as_ul(), '''<li>Headline: <input type="text" name="headline" maxlength="50" /></li>
<li>Slug: <input type="text" name="slug" maxlength="50" /></li>
@@ -1294,17 +1347,17 @@ class OldFormForXTests(TestCase):
self.assertEqual(list(ImprovedArticleWithParentLinkForm.base_fields), [])
- bw = BetterAuthor(name='Joe Better', score=10)
+ bw = BetterWriter(name='Joe Better', score=10)
bw.save()
self.assertEqual(sorted(model_to_dict(bw)),
- ['author_ptr', 'id', 'name', 'score'])
+ ['id', 'name', 'score', 'writer_ptr'])
- form = BetterAuthorForm({'name': 'Some Name', 'score': 12})
+ form = BetterWriterForm({'name': 'Some Name', 'score': 12})
self.assertEqual(form.is_valid(), True)
bw2 = form.save()
bw2.delete()
- form = AuthorProfileForm()
+ form = WriterProfileForm()
self.assertHTMLEqual(form.as_p(), '''<p><label for="id_writer">Writer:</label> <select name="writer" id="id_writer">
<option value="" selected="selected">---------</option>
<option value="%s">Bob Woodward</option>
@@ -1318,11 +1371,11 @@ class OldFormForXTests(TestCase):
'writer': six.text_type(w_woodward.pk),
'age': '65',
}
- form = AuthorProfileForm(data)
+ form = WriterProfileForm(data)
instance = form.save()
self.assertEqual(six.text_type(instance), 'Bob Woodward is 65')
- form = AuthorProfileForm(instance=instance)
+ form = WriterProfileForm(instance=instance)
self.assertHTMLEqual(form.as_p(), '''<p><label for="id_writer">Writer:</label> <select name="writer" id="id_writer">
<option value="">---------</option>
<option value="%s" selected="selected">Bob Woodward</option>
@@ -1715,6 +1768,18 @@ class OldFormForXTests(TestCase):
</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>"""
% {'blue_pk': colour.pk})
+ def test_custom_error_messages(self) :
+ data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'}
+ errors = CustomErrorMessageForm(data).errors
+ self.assertHTMLEqual(
+ str(errors['name1']),
+ '<ul class="errorlist"><li>Form custom error message.</li></ul>'
+ )
+ self.assertHTMLEqual(
+ str(errors['name2']),
+ '<ul class="errorlist"><li>Model custom error message.</li></ul>'
+ )
+
class M2mHelpTextTest(TestCase):
"""Tests for ticket #9321."""
diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py
index 80900a59e0..39ae857219 100644
--- a/tests/model_forms_regress/tests.py
+++ b/tests/model_forms_regress/tests.py
@@ -97,12 +97,14 @@ class PartiallyLocalizedTripleForm(forms.ModelForm):
class Meta:
model = Triple
localized_fields = ('left', 'right',)
+ fields = '__all__'
class FullyLocalizedTripleForm(forms.ModelForm):
class Meta:
model = Triple
- localized_fields = "__all__"
+ localized_fields = '__all__'
+ fields = '__all__'
class LocalizedModelFormTest(TestCase):
def test_model_form_applies_localize_to_some_fields(self):
diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py
index 8cfdf53995..03cd3b0159 100644
--- a/tests/model_formsets/tests.py
+++ b/tests/model_formsets/tests.py
@@ -1261,7 +1261,7 @@ class ModelFormsetTest(TestCase):
['Please correct the duplicate data for subtitle which must be unique for the month in posted.'])
-class TestModelFormsetWidgets(TestCase):
+class TestModelFormsetOverridesTroughFormMeta(TestCase):
def test_modelformset_factory_widgets(self):
widgets = {
'name': forms.TextInput(attrs={'class': 'poet'})
@@ -1283,3 +1283,53 @@ class TestModelFormsetWidgets(TestCase):
"%s" % form['title'],
'<input class="book" id="id_title" maxlength="100" name="title" type="text" />'
)
+
+ def test_modelformset_factory_labels_overrides(self):
+ BookFormSet = modelformset_factory(Book, fields="__all__", labels={
+ 'title': 'Name'
+ })
+ form = BookFormSet.form()
+ self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>')
+
+ def test_inlineformset_factory_labels_overrides(self):
+ BookFormSet = inlineformset_factory(Author, Book, fields="__all__", labels={
+ 'title': 'Name'
+ })
+ form = BookFormSet.form()
+ self.assertHTMLEqual(form['title'].label_tag(), '<label for="id_title">Name:</label>')
+
+ def test_modelformset_factory_help_text_overrides(self):
+ BookFormSet = modelformset_factory(Book, fields="__all__", help_texts={
+ 'title': 'Choose carefully.'
+ })
+ form = BookFormSet.form()
+ self.assertEqual(form['title'].help_text, 'Choose carefully.')
+
+ def test_inlineformset_factory_help_text_overrides(self):
+ BookFormSet = inlineformset_factory(Author, Book, fields="__all__", help_texts={
+ 'title': 'Choose carefully.'
+ })
+ form = BookFormSet.form()
+ self.assertEqual(form['title'].help_text, 'Choose carefully.')
+
+ def test_modelformset_factory_error_messages_overrides(self):
+ author = Author.objects.create(pk=1, name='Charles Baudelaire')
+ BookFormSet = modelformset_factory(Book, fields="__all__", error_messages={
+ 'title': {
+ 'max_length': 'Title too long!!'
+ }
+ })
+ form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
+ form.full_clean()
+ self.assertEqual(form.errors, {'title': ['Title too long!!']})
+
+ def test_inlineformset_factory_error_messages_overrides(self):
+ author = Author.objects.create(pk=1, name='Charles Baudelaire')
+ BookFormSet = inlineformset_factory(Author, Book, fields="__all__", error_messages={
+ 'title': {
+ 'max_length': 'Title too long!!'
+ }
+ })
+ form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id})
+ form.full_clean()
+ self.assertEqual(form.errors, {'title': ['Title too long!!']})
diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py
index e72fb6a4f9..a0d937ce81 100644
--- a/tests/multiple_database/tests.py
+++ b/tests/multiple_database/tests.py
@@ -1943,6 +1943,12 @@ class SyncOnlyDefaultDatabaseRouter(object):
class SyncDBTestCase(TestCase):
+
+ available_apps = [
+ 'multiple_database',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes'
+ ]
multi_db = True
def test_syncdb_to_other_database(self):
diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py
index 239bc67809..85bf74a4e9 100644
--- a/tests/proxy_model_inheritance/tests.py
+++ b/tests/proxy_model_inheritance/tests.py
@@ -22,6 +22,8 @@ class ProxyModelInheritanceTests(TransactionTestCase):
apps and calls syncdb, then verifies that the table has been created.
"""
+ available_apps = []
+
def setUp(self):
self.old_sys_path = sys.path[:]
sys.path.append(os.path.dirname(os.path.abspath(upath(__file__))))
@@ -38,7 +40,11 @@ class ProxyModelInheritanceTests(TransactionTestCase):
del cache.app_models['app2']
def test_table_exists(self):
- call_command('syncdb', verbosity=0)
+ try:
+ cache.set_available_apps(settings.INSTALLED_APPS)
+ call_command('syncdb', verbosity=0)
+ finally:
+ cache.unset_available_apps()
from .app1.models import ProxyModel
from .app2.models import NiceModel
self.assertEqual(NiceModel.objects.all().count(), 0)
diff --git a/tests/queries/tests.py b/tests/queries/tests.py
index 481b690c20..352e87a634 100644
--- a/tests/queries/tests.py
+++ b/tests/queries/tests.py
@@ -16,15 +16,16 @@ from django.test.utils import str_prefix
from django.utils import unittest
from django.utils.datastructures import SortedDict
-from .models import (Annotation, Article, Author, Celebrity, Child, Cover,
- Detail, DumbCategory, ExtraInfo, Fan, Item, LeafA, Join, LeafB, LoopX, LoopZ,
- ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA,
- Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
- Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
- SpecialCategory, OneToOneCategory, NullableName, ProxyCategory,
- SingleObject, RelatedObject, ModelA, ModelB, ModelC, ModelD, Responsibility,
- Job, JobResponsibilities, BaseA, Identifier, Program, Channel, Page,
- Paragraph, Chapter, Book, MyObject, Order, OrderItem)
+from .models import (
+ Annotation, Article, Author, Celebrity, Child, Cover, Detail, DumbCategory,
+ ExtraInfo, Fan, Item, LeafA, Join, LeafB, LoopX, LoopZ, ManagedModel,
+ Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related,
+ Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA,
+ ObjectB, ObjectC, CategoryItem, SimpleCategory, SpecialCategory,
+ OneToOneCategory, NullableName, ProxyCategory, SingleObject, RelatedObject,
+ ModelA, ModelB, ModelC, ModelD, Responsibility, Job, JobResponsibilities,
+ BaseA, FK1, Identifier, Program, Channel, Page, Paragraph, Chapter, Book,
+ MyObject, Order, OrderItem)
class BaseQuerysetTest(TestCase):
@@ -1975,13 +1976,62 @@ class EmptyQuerySetTests(TestCase):
class ValuesQuerysetTests(BaseQuerysetTest):
- def test_flat_values_lits(self):
+ def setUp(self):
Number.objects.create(num=72)
+ self.identity = lambda x: x
+
+ def test_flat_values_list(self):
qs = Number.objects.values_list("num")
qs = qs.values_list("num", flat=True)
- self.assertValueQuerysetEqual(
- qs, [72]
- )
+ self.assertValueQuerysetEqual(qs, [72])
+
+ def test_extra_values(self):
+ # testing for ticket 14930 issues
+ qs = Number.objects.extra(select=SortedDict([('value_plus_x', 'num+%s'),
+ ('value_minus_x', 'num-%s')]),
+ select_params=(1, 2))
+ qs = qs.order_by('value_minus_x')
+ qs = qs.values('num')
+ self.assertQuerysetEqual(qs, [{'num': 72}], self.identity)
+
+ def test_extra_values_order_twice(self):
+ # testing for ticket 14930 issues
+ qs = Number.objects.extra(select={'value_plus_one': 'num+1', 'value_minus_one': 'num-1'})
+ qs = qs.order_by('value_minus_one').order_by('value_plus_one')
+ qs = qs.values('num')
+ self.assertQuerysetEqual(qs, [{'num': 72}], self.identity)
+
+ def test_extra_values_order_multiple(self):
+ # Postgres doesn't allow constants in order by, so check for that.
+ qs = Number.objects.extra(select={
+ 'value_plus_one': 'num+1',
+ 'value_minus_one': 'num-1',
+ 'constant_value': '1'
+ })
+ qs = qs.order_by('value_plus_one', 'value_minus_one', 'constant_value')
+ qs = qs.values('num')
+ self.assertQuerysetEqual(qs, [{'num': 72}], self.identity)
+
+ def test_extra_values_order_in_extra(self):
+ # testing for ticket 14930 issues
+ qs = Number.objects.extra(
+ select={'value_plus_one': 'num+1', 'value_minus_one': 'num-1'},
+ order_by=['value_minus_one'])
+ qs = qs.values('num')
+
+ def test_extra_values_list(self):
+ # testing for ticket 14930 issues
+ qs = Number.objects.extra(select={'value_plus_one': 'num+1'})
+ qs = qs.order_by('value_plus_one')
+ qs = qs.values_list('num')
+ self.assertQuerysetEqual(qs, [(72,)], self.identity)
+
+ def test_flat_extra_values_list(self):
+ # testing for ticket 14930 issues
+ qs = Number.objects.extra(select={'value_plus_one': 'num+1'})
+ qs = qs.order_by('value_plus_one')
+ qs = qs.values_list('num', flat=True)
+ self.assertQuerysetEqual(qs, [72], self.identity)
class WeirdQuerysetSlicingTests(BaseQuerysetTest):
@@ -2620,6 +2670,19 @@ class JoinReuseTest(TestCase):
self.assertEqual(str(qs.query).count('JOIN'), 2)
class DisjunctionPromotionTests(TestCase):
+ def test_disjuction_promotion_select_related(self):
+ fk1 = FK1.objects.create(f1='f1', f2='f2')
+ basea = BaseA.objects.create(a=fk1)
+ qs = BaseA.objects.filter(Q(a=fk1) | Q(b=2))
+ self.assertEqual(str(qs.query).count(' JOIN '), 0)
+ qs = qs.select_related('a', 'b')
+ self.assertEqual(str(qs.query).count(' INNER JOIN '), 0)
+ self.assertEqual(str(qs.query).count(' LEFT OUTER JOIN '), 2)
+ with self.assertNumQueries(1):
+ self.assertQuerysetEqual(qs, [basea], lambda x: x)
+ self.assertEqual(qs[0].a, fk1)
+ self.assertIs(qs[0].b, None)
+
def test_disjunction_promotion1(self):
# Pre-existing join, add two ORed filters to the same join,
# all joins can be INNER JOINS.
@@ -2669,17 +2732,23 @@ class DisjunctionPromotionTests(TestCase):
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
- def test_disjunction_promotion4(self):
+ @unittest.expectedFailure
+ def test_disjunction_promotion4_failing(self):
+ # Failure because no join repromotion
qs = BaseA.objects.filter(Q(a=1) | Q(a=2))
self.assertEqual(str(qs.query).count('JOIN'), 0)
qs = qs.filter(a__f1='foo')
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
+
+ def test_disjunction_promotion4(self):
qs = BaseA.objects.filter(a__f1='foo')
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
qs = qs.filter(Q(a=1) | Q(a=2))
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
- def test_disjunction_promotion5(self):
+ @unittest.expectedFailure
+ def test_disjunction_promotion5_failing(self):
+ # Failure because no join repromotion logic.
qs = BaseA.objects.filter(Q(a=1) | Q(a=2))
# Note that the above filters on a force the join to an
# inner join even if it is trimmed.
@@ -2688,15 +2757,10 @@ class DisjunctionPromotionTests(TestCase):
# So, now the a__f1 join doesn't need promotion.
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
-
- @unittest.expectedFailure
- def test_disjunction_promotion5_failing(self):
qs = BaseA.objects.filter(Q(a__f1='foo') | Q(b__f1='foo'))
# Now the join to a is created as LOUTER
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 0)
- # The below filter should force the a to be inner joined. But,
- # this is failing as we do not have join unpromotion logic.
- qs = BaseA.objects.filter(Q(a=1) | Q(a=2))
+ qs = qs.objects.filter(Q(a=1) | Q(a=2))
self.assertEqual(str(qs.query).count('INNER JOIN'), 1)
self.assertEqual(str(qs.query).count('LEFT OUTER JOIN'), 1)
diff --git a/tests/queryset_pickle/models.py b/tests/queryset_pickle/models.py
index 4bcfcfbf04..3a8973505c 100644
--- a/tests/queryset_pickle/models.py
+++ b/tests/queryset_pickle/models.py
@@ -36,3 +36,13 @@ class Happening(models.Model):
number2 = models.IntegerField(blank=True, default=Numbers.get_static_number)
number3 = models.IntegerField(blank=True, default=Numbers.get_class_number)
number4 = models.IntegerField(blank=True, default=nn.get_member_number)
+
+class Container(object):
+ # To test pickling we need a class that isn't defined on module, but
+ # is still available from app-cache. So, the Container class moves
+ # SomeModel outside of module level
+ class SomeModel(models.Model):
+ somefield = models.IntegerField()
+
+class M2MModel(models.Model):
+ groups = models.ManyToManyField(Group)
diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py
index da1bb8cb73..b4b540c80d 100644
--- a/tests/queryset_pickle/tests.py
+++ b/tests/queryset_pickle/tests.py
@@ -3,9 +3,10 @@ from __future__ import absolute_import
import pickle
import datetime
+from django.db import models
from django.test import TestCase
-from .models import Group, Event, Happening
+from .models import Group, Event, Happening, Container, M2MModel
class PickleabilityTestCase(TestCase):
@@ -49,3 +50,43 @@ class PickleabilityTestCase(TestCase):
# can't just use assertEqual(original, unpickled)
self.assertEqual(original.__class__, unpickled.__class__)
self.assertEqual(original.args, unpickled.args)
+
+ def test_model_pickle(self):
+ """
+ Test that a model not defined on module level is pickleable.
+ """
+ original = Container.SomeModel(pk=1)
+ dumped = pickle.dumps(original)
+ reloaded = pickle.loads(dumped)
+ self.assertEqual(original, reloaded)
+ # Also, deferred dynamic model works
+ Container.SomeModel.objects.create(somefield=1)
+ original = Container.SomeModel.objects.defer('somefield')[0]
+ dumped = pickle.dumps(original)
+ reloaded = pickle.loads(dumped)
+ self.assertEqual(original, reloaded)
+ self.assertEqual(original.somefield, reloaded.somefield)
+
+ def test_model_pickle_m2m(self):
+ """
+ Test intentionally the automatically created through model.
+ """
+ m1 = M2MModel.objects.create()
+ g1 = Group.objects.create(name='foof')
+ m1.groups.add(g1)
+ m2m_through = M2MModel._meta.get_field_by_name('groups')[0].rel.through
+ original = m2m_through.objects.get()
+ dumped = pickle.dumps(original)
+ reloaded = pickle.loads(dumped)
+ self.assertEqual(original, reloaded)
+
+ def test_model_pickle_dynamic(self):
+ class Meta:
+ proxy = True
+ dynclass = type("DynamicEventSubclass", (Event, ),
+ {'Meta': Meta, '__module__': Event.__module__})
+ original = dynclass(pk=1)
+ dumped = pickle.dumps(original)
+ reloaded = pickle.loads(dumped)
+ self.assertEqual(original, reloaded)
+ self.assertIs(reloaded.__class__, dynclass)
diff --git a/tests/requests/tests.py b/tests/requests/tests.py
index 4d730bb561..d9120db4e7 100644
--- a/tests/requests/tests.py
+++ b/tests/requests/tests.py
@@ -683,6 +683,8 @@ class RequestsTests(SimpleTestCase):
"Cannot establish two connections to an in-memory SQLite database.")
class DatabaseConnectionHandlingTests(TransactionTestCase):
+ available_apps = []
+
def setUp(self):
# Use a temporary connection to avoid messing with the main one.
self._old_default_connection = connections['default']
diff --git a/tests/runtests.py b/tests/runtests.py
index a18324f676..da4592ecc0 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -5,6 +5,7 @@ import shutil
import subprocess
import sys
import tempfile
+import warnings
from django import contrib
from django.utils._os import upath
@@ -29,7 +30,6 @@ SUBDIRS_TO_SKIP = [
]
ALWAYS_INSTALLED_APPS = [
- 'shared_models',
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sites',
@@ -72,6 +72,15 @@ def get_installed():
def setup(verbosity, test_labels):
from django.conf import settings
from django.db.models.loading import get_apps, load_app
+ from django.test.testcases import TransactionTestCase, TestCase
+
+ # Force declaring available_apps in TransactionTestCase for faster tests.
+ def no_available_apps(self):
+ raise Exception("Please define available_apps in TransactionTestCase "
+ "and its subclasses.")
+ TransactionTestCase.available_apps = property(no_available_apps)
+ TestCase.available_apps = None
+
state = {
'INSTALLED_APPS': settings.INSTALLED_APPS,
'ROOT_URLCONF': getattr(settings, "ROOT_URLCONF", ""),
@@ -99,7 +108,9 @@ def setup(verbosity, test_labels):
logger.addHandler(handler)
# Load all the ALWAYS_INSTALLED_APPS.
- get_apps()
+ with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', 'django.contrib.comments is deprecated and will be removed before Django 1.8.', PendingDeprecationWarning)
+ get_apps()
# Load all the test model apps.
test_modules = get_test_modules()
diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py
index c2fa22705a..f087ca123a 100644
--- a/tests/select_for_update/tests.py
+++ b/tests/select_for_update/tests.py
@@ -23,6 +23,8 @@ requires_threading = unittest.skipUnless(threading, 'requires threading')
class SelectForUpdateTests(TransactionTestCase):
+ available_apps = ['select_for_update']
+
def setUp(self):
transaction.enter_transaction_management()
self.person = Person.objects.create(name='Reinhardt')
diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py
index a96a1af748..038276ca21 100644
--- a/tests/serializers/tests.py
+++ b/tests/serializers/tests.py
@@ -259,6 +259,9 @@ class SerializersTestBase(object):
class SerializersTransactionTestBase(object):
+
+ available_apps = ['serializers']
+
def test_forward_refs(self):
"""
Tests that objects ids can be referenced before they are
diff --git a/tests/servers/tests.py b/tests/servers/tests.py
index 920149477c..3377793d68 100644
--- a/tests/servers/tests.py
+++ b/tests/servers/tests.py
@@ -30,8 +30,15 @@ TEST_SETTINGS = {
class LiveServerBase(LiveServerTestCase):
- urls = 'servers.urls'
+
+ available_apps = [
+ 'servers',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ ]
fixtures = ['testdata.json']
+ urls = 'servers.urls'
@classmethod
def setUpClass(cls):
diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py
index 97e43ff0eb..f8d66bb2b7 100644
--- a/tests/settings_tests/tests.py
+++ b/tests/settings_tests/tests.py
@@ -12,6 +12,8 @@ from django.utils import unittest, six
@override_settings(TEST='override')
class FullyDecoratedTranTestCase(TransactionTestCase):
+ available_apps = []
+
def test_override(self):
self.assertEqual(settings.TEST, 'override')
diff --git a/tests/shared_models/models.py b/tests/shared_models/models.py
deleted file mode 100644
index 145edad1bf..0000000000
--- a/tests/shared_models/models.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from __future__ import unicode_literals
-
-from django.db import models
-from django.utils import timezone
-from django.utils.encoding import python_2_unicode_compatible
-
-
-class Tag(models.Model):
- name = models.CharField(max_length=255)
-
-
-@python_2_unicode_compatible
-class Author(models.Model):
- name = models.CharField(max_length=100, help_text='Use both first and last names.',
- unique=True)
-
- class Meta:
- ordering = ['name']
-
- def __str__(self):
- return self.name
-
-
-@python_2_unicode_compatible
-class Book(models.Model):
- title = models.CharField(max_length=200)
- pages = models.IntegerField(default=0)
- author = models.ForeignKey(Author, null=True, blank=True)
- pubdate = models.DateTimeField()
- tags = models.ManyToManyField(Tag)
-
- class Meta:
- ordering = ['-pubdate', 'title']
- unique_together = ['title', 'author']
-
- def __str__(self):
- return self.title
diff --git a/tests/signals_regress/models.py b/tests/signals_regress/models.py
index e69de29bb2..829314c06c 100644
--- a/tests/signals_regress/models.py
+++ b/tests/signals_regress/models.py
@@ -0,0 +1,18 @@
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class Author(models.Model):
+ name = models.CharField(max_length=20)
+
+ def __str__(self):
+ return self.name
+
+@python_2_unicode_compatible
+class Book(models.Model):
+ name = models.CharField(max_length=20)
+ authors = models.ManyToManyField(Author)
+
+ def __str__(self):
+ return self.name
diff --git a/tests/signals_regress/tests.py b/tests/signals_regress/tests.py
index b806d2116e..8fb3ad5a48 100644
--- a/tests/signals_regress/tests.py
+++ b/tests/signals_regress/tests.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import
from django.db import models
from django.test import TestCase
-from shared_models.models import Author, Book
+from .models import Author, Book
class SignalsRegressTests(TestCase):
@@ -77,7 +77,7 @@ class SignalsRegressTests(TestCase):
"Is created"
])
- b1 = Book(title='Snow Crash', pubdate='2012-02-02 12:00')
+ b1 = Book(name='Snow Crash')
self.assertEqual(self.get_signal_output(b1.save), [
"pre_save signal, Snow Crash",
"post_save signal, Snow Crash",
@@ -87,7 +87,7 @@ class SignalsRegressTests(TestCase):
def test_m2m_signals(self):
""" Assigning and removing to/from m2m shouldn't generate an m2m signal """
- b1 = Book(title='Snow Crash', pubdate='2012-02-02 12:00')
+ b1 = Book(name='Snow Crash')
self.get_signal_output(b1.save)
a1 = Author(name='Neal Stephenson')
self.get_signal_output(a1.save)
diff --git a/tests/swappable_models/tests.py b/tests/swappable_models/tests.py
index d3d61341d2..858061db23 100644
--- a/tests/swappable_models/tests.py
+++ b/tests/swappable_models/tests.py
@@ -13,6 +13,13 @@ from swappable_models.models import Article
class SwappableModelTests(TestCase):
+
+ available_apps = [
+ 'swappable_models',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ ]
+
def setUp(self):
# This test modifies the installed apps, so we need to make sure
# we're not dealing with a cached app list.
diff --git a/tests/syncdb_signals/tests.py b/tests/syncdb_signals/tests.py
index d9be3b65d4..d70fb2c2c0 100644
--- a/tests/syncdb_signals/tests.py
+++ b/tests/syncdb_signals/tests.py
@@ -3,7 +3,7 @@ from django.test import TestCase
from django.core import management
from django.utils import six
-from shared_models import models
+from . import models
PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db']
@@ -55,6 +55,11 @@ signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models)
class SyncdbSignalTests(TestCase):
+
+ available_apps = [
+ 'syncdb_signals',
+ ]
+
def test_pre_syncdb_call_time(self):
self.assertEqual(pre_syncdb_receiver.call_counter, 1)
diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py
index 95ccf0242a..0dc7a0155a 100644
--- a/tests/test_runner/tests.py
+++ b/tests/test_runner/tests.py
@@ -9,9 +9,9 @@ from optparse import make_option
from django.core.exceptions import ImproperlyConfigured
from django.core.management import call_command
from django import db
-from django.test import runner, TransactionTestCase, skipUnlessDBFeature
-from django.test.simple import DjangoTestSuiteRunner, get_tests
+from django.test import runner, TestCase, TransactionTestCase, skipUnlessDBFeature
from django.test.testcases import connections_support_transactions
+from django.test.utils import IgnorePendingDeprecationWarningsMixin
from django.utils import unittest
from django.utils.importlib import import_module
@@ -225,20 +225,24 @@ class Ticket17477RegressionTests(AdminScriptTestCase):
self.assertNoOutput(err)
-class ModulesTestsPackages(unittest.TestCase):
+class ModulesTestsPackages(IgnorePendingDeprecationWarningsMixin, unittest.TestCase):
def test_get_tests(self):
"Check that the get_tests helper function can find tests in a directory"
+ from django.test.simple import get_tests
module = import_module(TEST_APP_OK)
tests = get_tests(module)
self.assertIsInstance(tests, type(module))
def test_import_error(self):
"Test for #12658 - Tests with ImportError's shouldn't fail silently"
+ from django.test.simple import get_tests
module = import_module(TEST_APP_ERROR)
self.assertRaises(ImportError, get_tests, module)
-class Sqlite3InMemoryTestDbs(unittest.TestCase):
+class Sqlite3InMemoryTestDbs(TestCase):
+
+ available_apps = []
@unittest.skipUnless(all(db.connections[conn].vendor == 'sqlite' for conn in db.connections),
"This is a sqlite-specific issue")
@@ -258,7 +262,7 @@ class Sqlite3InMemoryTestDbs(unittest.TestCase):
},
})
other = db.connections['other']
- DjangoTestSuiteRunner(verbosity=0).setup_databases()
+ runner.DiscoverRunner(verbosity=0).setup_databases()
msg = "DATABASES setting '%s' option set to sqlite3's ':memory:' value shouldn't interfere with transaction support detection." % option
# Transaction support should be properly initialised for the 'other' DB
self.assertTrue(other.features.supports_transactions, msg)
@@ -273,12 +277,12 @@ class DummyBackendTest(unittest.TestCase):
"""
Test that setup_databases() doesn't fail with dummy database backend.
"""
- runner = DjangoTestSuiteRunner(verbosity=0)
+ runner_instance = runner.DiscoverRunner(verbosity=0)
old_db_connections = db.connections
try:
db.connections = db.ConnectionHandler({})
- old_config = runner.setup_databases()
- runner.teardown_databases(old_config)
+ old_config = runner_instance.setup_databases()
+ runner_instance.teardown_databases(old_config)
except Exception as e:
self.fail("setup_databases/teardown_databases unexpectedly raised "
"an error: %s" % e)
@@ -318,6 +322,8 @@ class AutoIncrementResetTest(TransactionTestCase):
that AutoField values start from 1 for each transactional test case.
"""
+ available_apps = ['test_runner']
+
reset_sequences = True
@skipUnlessDBFeature('supports_sequence_reset')
diff --git a/tests/test_suite_override/tests.py b/tests/test_suite_override/tests.py
index dea110434f..35ca2b060b 100644
--- a/tests/test_suite_override/tests.py
+++ b/tests/test_suite_override/tests.py
@@ -1,5 +1,5 @@
from django.db.models import get_app
-from django.test.simple import build_suite
+from django.test.utils import IgnorePendingDeprecationWarningsMixin
from django.utils import unittest
@@ -9,7 +9,7 @@ def suite():
return testSuite
-class SuiteOverrideTest(unittest.TestCase):
+class SuiteOverrideTest(IgnorePendingDeprecationWarningsMixin, unittest.TestCase):
def test_suite_override(self):
"""
Validate that you can define a custom suite when running tests with
@@ -17,6 +17,7 @@ class SuiteOverrideTest(unittest.TestCase):
suite using ``build_suite``).
"""
+ from django.test.simple import build_suite
app = get_app("test_suite_override")
suite = build_suite(app)
self.assertEqual(suite.countTestCases(), 1)
diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py
index d72a482819..5f7fb05eb4 100644
--- a/tests/test_utils/tests.py
+++ b/tests/test_utils/tests.py
@@ -8,8 +8,7 @@ from django.http import HttpResponse
from django.template.loader import render_to_string
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from django.test.html import HTMLParseError, parse_html
-from django.test.simple import make_doctest
-from django.test.utils import CaptureQueriesContext
+from django.test.utils import CaptureQueriesContext, IgnorePendingDeprecationWarningsMixin
from django.utils import six
from django.utils import unittest
from django.utils.unittest import skip
@@ -624,9 +623,10 @@ class AssertFieldOutputTests(SimpleTestCase):
self.assertFieldOutput(MyCustomField, {}, {}, empty_value=None)
-class DoctestNormalizerTest(SimpleTestCase):
+class DoctestNormalizerTest(IgnorePendingDeprecationWarningsMixin, SimpleTestCase):
def test_normalizer(self):
+ from django.test.simple import make_doctest
suite = make_doctest("test_utils.doctest_output")
failures = unittest.TextTestRunner(stream=six.StringIO()).run(suite)
self.assertEqual(failures.failures, [])
diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py
index 0f16a9c805..24b7615d6f 100644
--- a/tests/transactions/tests.py
+++ b/tests/transactions/tests.py
@@ -26,6 +26,8 @@ class AtomicTests(TransactionTestCase):
syntax and the bulk of the tests use the context manager syntax.
"""
+ available_apps = ['transactions']
+
def test_decorator_syntax_commit(self):
@transaction.atomic
def make_reporter():
@@ -232,6 +234,8 @@ class AtomicInsideLegacyTransactionManagementTests(AtomicTests):
class AtomicMergeTests(TransactionTestCase):
"""Test merging transactions with savepoint=False."""
+ available_apps = ['transactions']
+
def test_merged_outer_rollback(self):
with transaction.atomic():
Reporter.objects.create(first_name="Tintin")
@@ -286,6 +290,8 @@ class AtomicMergeTests(TransactionTestCase):
"'atomic' requires transactions and savepoints.")
class AtomicErrorsTests(TransactionTestCase):
+ available_apps = []
+
def test_atomic_prevents_setting_autocommit(self):
autocommit = transaction.get_autocommit()
with transaction.atomic():
@@ -311,6 +317,8 @@ class AtomicErrorsTests(TransactionTestCase):
class AtomicMiscTests(TransactionTestCase):
+ available_apps = []
+
def test_wrap_callable_instance(self):
# Regression test for #20028
class Callable(object):
@@ -322,6 +330,8 @@ class AtomicMiscTests(TransactionTestCase):
class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
+ available_apps = ['transactions']
+
def create_a_reporter_then_fail(self, first, last):
a = Reporter(first_name=first, last_name=last)
a.save()
@@ -477,6 +487,9 @@ class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCas
class TransactionRollbackTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
+
+ available_apps = ['transactions']
+
def execute_bad_sql(self):
cursor = connection.cursor()
cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');")
@@ -494,6 +507,9 @@ class TransactionRollbackTests(IgnorePendingDeprecationWarningsMixin, Transactio
transaction.rollback()
class TransactionContextManagerTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
+
+ available_apps = ['transactions']
+
def create_reporter_and_fail(self):
Reporter.objects.create(first_name="Bob", last_name="Holtzman")
raise Exception
diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py
index 5339b4a8ea..8078f1d128 100644
--- a/tests/transactions_regress/tests.py
+++ b/tests/transactions_regress/tests.py
@@ -10,6 +10,9 @@ from django.utils.unittest import skipIf, skipUnless
from .models import Mod, M2mA, M2mB, SubMod
class ModelInheritanceTests(TransactionTestCase):
+
+ available_apps = ['transactions_regress']
+
def test_save(self):
# First, create a SubMod, then try to save another with conflicting
# cnt field. The problem was that transactions were committed after
@@ -33,6 +36,13 @@ class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionT
when they should be, and aren't left pending after operations
have been performed in them. Refs #9964.
"""
+
+ available_apps = [
+ 'transactions_regress',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ ]
+
def test_raw_committed_on_success(self):
"""
Make sure a transaction consisting of raw SQL execution gets
@@ -188,6 +198,9 @@ class TestNewConnection(IgnorePendingDeprecationWarningsMixin, TransactionTestCa
"""
Check that new connections don't have special behaviour.
"""
+
+ available_apps = ['transactions_regress']
+
def setUp(self):
self._old_backend = connections[DEFAULT_DB_ALIAS]
settings = self._old_backend.settings_dict.copy()
@@ -235,6 +248,9 @@ class TestPostgresAutocommitAndIsolation(IgnorePendingDeprecationWarningsMixin,
is restored after entering and leaving transaction management.
Refs #16047, #18130.
"""
+
+ available_apps = ['transactions_regress']
+
def setUp(self):
from psycopg2.extensions import (ISOLATION_LEVEL_AUTOCOMMIT,
ISOLATION_LEVEL_SERIALIZABLE,
@@ -311,6 +327,9 @@ class TestPostgresAutocommitAndIsolation(IgnorePendingDeprecationWarningsMixin,
class TestManyToManyAddTransaction(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
+
+ available_apps = ['transactions_regress']
+
def test_manyrelated_add_commit(self):
"Test for https://code.djangoproject.com/ticket/16818"
a = M2mA.objects.create()
@@ -327,6 +346,8 @@ class TestManyToManyAddTransaction(IgnorePendingDeprecationWarningsMixin, Transa
class SavepointTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase):
+ available_apps = ['transactions_regress']
+
@skipIf(connection.vendor == 'sqlite',
"SQLite doesn't support savepoints in managed mode")
@skipUnlessDBFeature('uses_savepoints')
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index f54c796d30..3962f69288 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -192,6 +192,20 @@ class URLPatternReverse(TestCase):
self.assertEqual('/%7Eme/places/1/',
reverse('places', args=[1], prefix='/~me/'))
+ def test_patterns_reported(self):
+ # Regression for #17076
+ try:
+ # this url exists, but requires an argument
+ reverse("people", args=[])
+ except NoReverseMatch as e:
+ pattern_description = r"1 pattern(s) tried: ['people/(?P<name>\\w+)/$']"
+ self.assertIn(pattern_description, str(e))
+ else:
+ # we can't use .assertRaises, since we want to inspect the
+ # exception
+ self.fail("Expected a NoReverseMatch, but none occurred.")
+
+
class ResolverTests(unittest.TestCase):
def test_resolver_repr(self):
"""
diff --git a/tests/validation/models.py b/tests/validation/models.py
index e95a1e0744..958740dd2d 100644
--- a/tests/validation/models.py
+++ b/tests/validation/models.py
@@ -19,6 +19,7 @@ class ModelToValidate(models.Model):
email = models.EmailField(blank=True)
url = models.URLField(blank=True)
f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe])
+ slug = models.SlugField(blank=True)
def clean(self):
super(ModelToValidate, self).clean()
diff --git a/tests/validation/tests.py b/tests/validation/tests.py
index 9ddf796c2b..c8b679541a 100644
--- a/tests/validation/tests.py
+++ b/tests/validation/tests.py
@@ -53,7 +53,11 @@ class BaseModelValidationTests(ValidationTestCase):
def test_text_greater_that_charfields_max_length_raises_erros(self):
mtv = ModelToValidate(number=10, name='Some Name'*100)
- self.assertFailsValidation(mtv.full_clean, ['name',])
+ self.assertFailsValidation(mtv.full_clean, ['name'])
+
+ def test_malformed_slug_raises_error(self):
+ mtv = ModelToValidate(number=10, name='Some Name', slug='##invalid##')
+ self.assertFailsValidation(mtv.full_clean, ['slug'])
class ArticleForm(forms.ModelForm):
diff --git a/tests/validators/tests.py b/tests/validators/tests.py
index 6b46c53cc3..a1555d8e91 100644
--- a/tests/validators/tests.py
+++ b/tests/validators/tests.py
@@ -213,9 +213,9 @@ class TestSimpleValidators(TestCase):
self.assertEqual(repr(v), str_prefix("ValidationError([%(_)s'First Problem', %(_)s'Second Problem'])"))
def test_message_dict(self):
- v = ValidationError({'first': 'First Problem'})
- self.assertEqual(str(v), str_prefix("{%(_)s'first': %(_)s'First Problem'}"))
- self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': %(_)s'First Problem'})"))
+ v = ValidationError({'first': ['First Problem']})
+ self.assertEqual(str(v), str_prefix("{%(_)s'first': [%(_)s'First Problem']}"))
+ self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': [%(_)s'First Problem']})"))
test_counter = 0
for validator, value, expected in TEST_DATA:
diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py
index 2da17dd0f5..b35b192fd0 100644
--- a/tests/view_tests/tests/test_i18n.py
+++ b/tests/view_tests/tests/test_i18n.py
@@ -183,6 +183,8 @@ skip_selenium = not os.environ.get('DJANGO_SELENIUM_TESTS', False)
@unittest.skipIf(skip_selenium, 'Selenium tests not requested')
@unittest.skipUnless(firefox, 'Selenium not installed')
class JavascriptI18nTests(LiveServerTestCase):
+
+ available_apps = []
urls = 'view_tests.urls'
@classmethod