diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-08-09 14:17:30 +0100 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-08-09 14:17:30 +0100 |
| commit | de64c4d6e97c980fb4c0ace045fc4070b3f763d9 (patch) | |
| tree | 045c8af6a88fa964cf7e284970a6e93d53941a79 | |
| parent | fddc5957c53bd654312c4a238a8cdcfe5f4ef4cc (diff) | |
| parent | b575d690bbc1c4cd7f575346132c09fca8c736a7 (diff) | |
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts:
django/core/management/commands/flush.py
django/core/management/commands/syncdb.py
django/db/models/loading.py
docs/internals/deprecation.txt
docs/ref/django-admin.txt
docs/releases/1.7.txt
489 files changed, 3840 insertions, 1593 deletions
@@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better: Paul Collier <paul@paul-collier.com> Paul Collins <paul.collins.iii@gmail.com> Robert Coup + Alex Couper <http://alexcouper.com/> Deric Crago <deric.crago@gmail.com> Brian Fabian Crain <http://www.bfc.do/> David Cramer <dcramer@gmail.com> @@ -416,6 +417,7 @@ answer newbie questions, and generally made Django that much better: Zain Memon Christian Metts michal@plovarna.cz + Justin Michalicek <jmichalicek@gmail.com> Slawek Mikula <slawek dot mikula at gmail dot com> Katie Miller <katie@sub50.com> Shawn Milochik <shawn@milochik.com> @@ -542,6 +544,7 @@ answer newbie questions, and generally made Django that much better: smurf@smurf.noris.de Vsevolod Solovyov George Song <george@damacy.net> + Jimmy Song <jaejoon@gmail.com> sopel Leo Soto <leo.soto@gmail.com> Thomas Sorrel diff --git a/django/conf/__init__.py b/django/conf/__init__.py index c4b9634d83..e628af748c 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -6,6 +6,7 @@ variable, and then from django.conf.global_settings; see the global settings fil a list of all possible variables. """ +import importlib import logging import os import sys @@ -15,7 +16,6 @@ import warnings from django.conf import global_settings from django.core.exceptions import ImproperlyConfigured from django.utils.functional import LazyObject, empty -from django.utils import importlib from django.utils.module_loading import import_by_path from django.utils import six @@ -107,6 +107,9 @@ class BaseSettings(object): elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types): raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set " "to a tuple, not a string.") + elif name == "INSTALLED_APPS" and len(value) != len(set(value)): + raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique values.") + object.__setattr__(self, name, value) diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index c0340c0543..f4a129a9e0 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -1,7 +1,8 @@ +from importlib import import_module + from django.core.urlresolvers import (RegexURLPattern, RegexURLResolver, LocaleRegexURLResolver) from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module from django.utils import six diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index 1084784003..ef2e87407a 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -17,8 +17,8 @@ def autodiscover(): """ import copy + from importlib import import_module from django.conf import settings - from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule for app in settings.INSTALLED_APPS: diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index 4131494515..3a37de2404 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -87,7 +87,7 @@ class SimpleListFilter(ListFilter): def lookups(self, request, model_admin): """ - Must be overriden to return a list of tuples (value, verbose value) + Must be overridden to return a list of tuples (value, verbose value) """ raise NotImplementedError diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 3ffb85e6c6..b6d5bde932 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -125,14 +125,16 @@ class AdminField(object): contents = conditional_escape(force_text(self.field.label)) if self.is_checkbox: classes.append('vCheckboxLabel') - else: - contents += ':' + if self.field.field.required: classes.append('required') if not self.is_first: classes.append('inline') attrs = {'class': ' '.join(classes)} if classes else {} - return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) + # checkboxes should not have a label suffix as the checkbox appears + # to the left of the label. + return self.field.label_tag(contents=mark_safe(contents), attrs=attrs, + label_suffix='' if self.is_checkbox else None) def errors(self): return mark_safe(self.field.errors.as_ul()) diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index f3be6530bf..dc282b7e57 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -4,7 +4,7 @@ from django.db import models from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.admin.util import quote -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, NoReverseMatch from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.encoding import smart_text from django.utils.encoding import python_2_unicode_compatible @@ -74,5 +74,8 @@ class LogEntry(models.Model): """ if self.content_type and self.object_id: url_name = 'admin:%s_%s_change' % (self.content_type.app_label, self.content_type.model) - return reverse(url_name, args=(quote(self.object_id),)) + try: + return reverse(url_name, args=(quote(self.object_id),)) + except NoReverseMatch: + pass return None diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index afc7cfc5bd..b475868598 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,6 +1,8 @@ +from collections import OrderedDict import copy import operator from functools import partial, reduce, update_wrapper +import warnings from django import forms from django.conf import settings @@ -29,7 +31,6 @@ from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils.decorators import method_decorator -from django.utils.datastructures import SortedDict from django.utils.html import escape, escapejs from django.utils.safestring import mark_safe from django.utils import six @@ -69,6 +70,7 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = { models.CharField: {'widget': widgets.AdminTextInputWidget}, models.ImageField: {'widget': widgets.AdminFileWidget}, models.FileField: {'widget': widgets.AdminFileWidget}, + models.EmailField: {'widget': widgets.AdminEmailInputWidget}, } csrf_protect_m = method_decorator(csrf_protect) @@ -237,13 +239,49 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): return db_field.formfield(**kwargs) - def _declared_fieldsets(self): + @property + def declared_fieldsets(self): + warnings.warn( + "ModelAdmin.declared_fieldsets is deprecated and " + "will be removed in Django 1.9.", + PendingDeprecationWarning, stacklevel=2 + ) + if self.fieldsets: return self.fieldsets elif self.fields: return [(None, {'fields': self.fields})] return None - declared_fieldsets = property(_declared_fieldsets) + + def get_fields(self, request, obj=None): + """ + Hook for specifying fields. + """ + return self.fields + + def get_fieldsets(self, request, obj=None): + """ + Hook for specifying fieldsets. + """ + # We access the property and check if it triggers a warning. + # If it does, then it's ours and we can safely ignore it, but if + # it doesn't then it has been overriden so we must warn about the + # deprecation. + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + declared_fieldsets = self.declared_fieldsets + if len(w) != 1 or not issubclass(w[0].category, PendingDeprecationWarning): + warnings.warn( + "ModelAdmin.declared_fieldsets is deprecated and " + "will be removed in Django 1.9.", + PendingDeprecationWarning + ) + if declared_fieldsets: + return declared_fieldsets + + if self.fieldsets: + return self.fieldsets + return [(None, {'fields': self.get_fields(request, obj)})] def get_ordering(self, request): """ @@ -263,34 +301,6 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): """ return self.prepopulated_fields - def get_search_results(self, request, queryset, search_term): - # Apply keyword searches. - def construct_search(field_name): - if field_name.startswith('^'): - return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): - return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): - return "%s__search" % field_name[1:] - else: - return "%s__icontains" % field_name - - use_distinct = False - if self.search_fields and search_term: - orm_lookups = [construct_search(str(search_field)) - for search_field in self.search_fields] - for bit in search_term.split(): - or_queries = [models.Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] - queryset = queryset.filter(reduce(operator.or_, or_queries)) - if not use_distinct: - for search_spec in orm_lookups: - if lookup_needs_distinct(self.opts, search_spec): - use_distinct = True - break - - return queryset, use_distinct - def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the @@ -505,13 +515,11 @@ class ModelAdmin(BaseModelAdmin): 'delete': self.has_delete_permission(request), } - def get_fieldsets(self, request, obj=None): - "Hook for specifying fieldsets for the add form." - if self.declared_fieldsets: - return self.declared_fieldsets + def get_fields(self, request, obj=None): + if self.fields: + return self.fields form = self.get_form(request, obj, fields=None) - fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) - return [(None, {'fields': fields})] + return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) def get_form(self, request, obj=None, **kwargs): """ @@ -670,7 +678,7 @@ class ModelAdmin(BaseModelAdmin): # want *any* actions enabled on this page. from django.contrib.admin.views.main import _is_changelist_popup if self.actions is None or _is_changelist_popup(request): - return SortedDict() + return OrderedDict() actions = [] @@ -691,8 +699,8 @@ class ModelAdmin(BaseModelAdmin): # get_action might have returned None, so filter any of those out. actions = filter(None, actions) - # Convert the actions into a SortedDict keyed by name. - actions = SortedDict([ + # Convert the actions into an OrderedDict keyed by name. + actions = OrderedDict([ (name, (func, name, desc)) for func, name, desc in actions ]) @@ -766,11 +774,50 @@ class ModelAdmin(BaseModelAdmin): """ return self.list_filter + def get_search_fields(self, request): + """ + Returns a sequence containing the fields to be searched whenever + somebody submits a search query. + """ + return self.search_fields + + def get_search_results(self, request, queryset, search_term): + """ + Returns a tuple containing a queryset to implement the search, + and a boolean indicating if the results may contain duplicates. + """ + # Apply keyword searches. + def construct_search(field_name): + if field_name.startswith('^'): + return "%s__istartswith" % field_name[1:] + elif field_name.startswith('='): + return "%s__iexact" % field_name[1:] + elif field_name.startswith('@'): + return "%s__search" % field_name[1:] + else: + return "%s__icontains" % field_name + + use_distinct = False + search_fields = self.get_search_fields(request) + if search_fields and search_term: + orm_lookups = [construct_search(str(search_field)) + for search_field in search_fields] + for bit in search_term.split(): + or_queries = [models.Q(**{orm_lookup: bit}) + for orm_lookup in orm_lookups] + queryset = queryset.filter(reduce(operator.or_, or_queries)) + if not use_distinct: + for search_spec in orm_lookups: + if lookup_needs_distinct(self.opts, search_spec): + use_distinct = True + break + + return queryset, use_distinct + def get_preserved_filters(self, request): """ Returns the preserved filters querystring. """ - match = request.resolver_match if self.preserve_filters and match: opts = self.model._meta @@ -1106,17 +1153,7 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = self.model() - prefixes = {} - for FormSet, inline in zip(self.get_formsets(request), inline_instances): - prefix = FormSet.get_default_prefix() - prefixes[prefix] = prefixes.get(prefix, 0) + 1 - if prefixes[prefix] != 1 or not prefix: - prefix = "%s-%s" % (prefix, prefixes[prefix]) - formset = FormSet(data=request.POST, files=request.FILES, - instance=new_object, - save_as_new="_saveasnew" in request.POST, - prefix=prefix, queryset=inline.get_queryset(request)) - formsets.append(formset) + formsets = self._create_formsets(request, new_object, inline_instances) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, False) self.save_related(request, form, formsets, False) @@ -1134,15 +1171,7 @@ class ModelAdmin(BaseModelAdmin): if isinstance(f, models.ManyToManyField): initial[k] = initial[k].split(",") form = ModelForm(initial=initial) - prefixes = {} - for FormSet, inline in zip(self.get_formsets(request), inline_instances): - prefix = FormSet.get_default_prefix() - prefixes[prefix] = prefixes.get(prefix, 0) + 1 - if prefixes[prefix] != 1 or not prefix: - prefix = "%s-%s" % (prefix, prefixes[prefix]) - formset = FormSet(instance=self.model(), prefix=prefix, - queryset=inline.get_queryset(request)) - formsets.append(formset) + formsets = self._create_formsets(request, self.model(), inline_instances) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.get_prepopulated_fields(request), @@ -1194,7 +1223,6 @@ class ModelAdmin(BaseModelAdmin): current_app=self.admin_site.name)) ModelForm = self.get_form(request, obj) - formsets = [] inline_instances = self.get_inline_instances(request, obj) if request.method == 'POST': form = ModelForm(request.POST, request.FILES, instance=obj) @@ -1204,18 +1232,7 @@ class ModelAdmin(BaseModelAdmin): else: form_validated = False new_object = obj - prefixes = {} - for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances): - prefix = FormSet.get_default_prefix() - prefixes[prefix] = prefixes.get(prefix, 0) + 1 - if prefixes[prefix] != 1 or not prefix: - prefix = "%s-%s" % (prefix, prefixes[prefix]) - formset = FormSet(request.POST, request.FILES, - instance=new_object, prefix=prefix, - queryset=inline.get_queryset(request)) - - formsets.append(formset) - + formsets = self._create_formsets(request, new_object, inline_instances) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, True) self.save_related(request, form, formsets, True) @@ -1225,15 +1242,7 @@ class ModelAdmin(BaseModelAdmin): else: form = ModelForm(instance=obj) - prefixes = {} - for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances): - prefix = FormSet.get_default_prefix() - prefixes[prefix] = prefixes.get(prefix, 0) + 1 - if prefixes[prefix] != 1 or not prefix: - prefix = "%s-%s" % (prefix, prefixes[prefix]) - formset = FormSet(instance=obj, prefix=prefix, - queryset=inline.get_queryset(request)) - formsets.append(formset) + formsets = self._create_formsets(request, obj, inline_instances) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.get_prepopulated_fields(request, obj), @@ -1280,6 +1289,7 @@ class ModelAdmin(BaseModelAdmin): list_display = self.get_list_display(request) list_display_links = self.get_list_display_links(request, list_display) list_filter = self.get_list_filter(request) + search_fields = self.get_search_fields(request) # Check actions to see if any are available on this changelist actions = self.get_actions(request) @@ -1291,9 +1301,9 @@ class ModelAdmin(BaseModelAdmin): try: cl = ChangeList(request, self.model, list_display, list_display_links, list_filter, self.date_hierarchy, - self.search_fields, self.list_select_related, - self.list_per_page, self.list_max_show_all, self.list_editable, - self) + search_fields, self.list_select_related, self.list_per_page, + self.list_max_show_all, self.list_editable, self) + except IncorrectLookupParameters: # Wacky lookup parameters were given, so redirect to the main # changelist page, without parameters, and pass an 'invalid=1' @@ -1532,6 +1542,32 @@ class ModelAdmin(BaseModelAdmin): "admin/object_history.html" ], context, current_app=self.admin_site.name) + def _create_formsets(self, request, obj, inline_instances): + "Helper function to generate formsets for add/change_view." + formsets = [] + prefixes = {} + get_formsets_args = [request] + if obj.pk: + get_formsets_args.append(obj) + for FormSet, inline in zip(self.get_formsets(*get_formsets_args), inline_instances): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1 or not prefix: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset_params = { + 'instance': obj, + 'prefix': prefix, + 'queryset': inline.get_queryset(request), + } + if request.method == 'POST': + formset_params.update({ + 'data': request.POST, + 'files': request.FILES, + 'save_as_new': '_saveasnew' in request.POST + }) + formsets.append(FormSet(**formset_params)) + return formsets + class InlineModelAdmin(BaseModelAdmin): """ @@ -1656,12 +1692,11 @@ class InlineModelAdmin(BaseModelAdmin): return inlineformset_factory(self.parent_model, self.model, **defaults) - def get_fieldsets(self, request, obj=None): - if self.declared_fieldsets: - return self.declared_fieldsets + def get_fields(self, request, obj=None): + if self.fields: + return self.fields form = self.get_formset(request, obj, fields=None).form - fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) - return [(None, {'fields': fields})] + return list(form.base_fields) + list(self.get_readonly_fields(request, obj)) def get_queryset(self, request): queryset = super(InlineModelAdmin, self).get_queryset(request) diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css index 1439b5d675..9fef3a8bc1 100644 --- a/django/contrib/admin/static/admin/css/base.css +++ b/django/contrib/admin/static/admin/css/base.css @@ -661,45 +661,34 @@ a.deletelink:hover { .object-tools li { display: block; float: left; - background: url(../img/tool-left.gif) 0 0 no-repeat; - padding: 0 0 0 8px; - margin-left: 2px; + margin-left: 5px; height: 16px; } -.object-tools li:hover { - background: url(../img/tool-left_over.gif) 0 0 no-repeat; +.object-tools a { + border-radius: 15px; } .object-tools a:link, .object-tools a:visited { display: block; float: left; color: white; - padding: .1em 14px .1em 8px; - height: 14px; - background: #999 url(../img/tool-right.gif) 100% 0 no-repeat; + padding: .2em 10px; + background: #999; } .object-tools a:hover, .object-tools li:hover a { - background: #5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat; + background-color: #5b80b2; } .object-tools a.viewsitelink, .object-tools a.golink { - background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat; - padding-right: 28px; -} - -.object-tools a.viewsitelink:hover, .object-tools a.golink:hover { - background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat; + background: #999 url(../img/tooltag-arrowright.png) 95% center no-repeat; + padding-right: 26px; } .object-tools a.addlink { - background: #999 url(../img/tooltag-add.gif) top right no-repeat; - padding-right: 28px; -} - -.object-tools a.addlink:hover { - background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat; + background: #999 url(../img/tooltag-add.png) 95% center no-repeat; + padding-right: 26px; } /* OBJECT HISTORY */ @@ -837,4 +826,3 @@ table#change-history tbody th { background: #eee url(../img/nav-bg.gif) bottom left repeat-x; color: #666; } - diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css index d61cd3a218..56817228f3 100644 --- a/django/contrib/admin/static/admin/css/widgets.css +++ b/django/contrib/admin/static/admin/css/widgets.css @@ -54,8 +54,8 @@ .selector ul.selector-chooser { float: left; width: 22px; - height: 50px; - background: url(../img/chooser-bg.gif) top center no-repeat; + background-color: #eee; + border-radius: 10px; margin: 10em 5px 0 5px; padding: 0; } @@ -169,7 +169,8 @@ a.active.selector-clearall { height: 22px; width: 50px; margin: 0 0 3px 40%; - background: url(../img/chooser_stacked-bg.gif) top center no-repeat; + background-color: #eee; + border-radius: 10px; } .stacked .selector-chooser li { @@ -575,4 +576,3 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover { font-size: 11px; border-top: 1px solid #ddd; } - diff --git a/django/contrib/admin/static/admin/img/chooser-bg.gif b/django/contrib/admin/static/admin/img/chooser-bg.gif Binary files differdeleted file mode 100644 index 30e83c2518..0000000000 --- a/django/contrib/admin/static/admin/img/chooser-bg.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif b/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif Binary files differdeleted file mode 100644 index 5d104b6d98..0000000000 --- a/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tool-left.gif b/django/contrib/admin/static/admin/img/tool-left.gif Binary files differdeleted file mode 100644 index 011490ff3a..0000000000 --- a/django/contrib/admin/static/admin/img/tool-left.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tool-left_over.gif b/django/contrib/admin/static/admin/img/tool-left_over.gif Binary files differdeleted file mode 100644 index 937e07bb1a..0000000000 --- a/django/contrib/admin/static/admin/img/tool-left_over.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tool-right.gif b/django/contrib/admin/static/admin/img/tool-right.gif Binary files differdeleted file mode 100644 index cdc140cc59..0000000000 --- a/django/contrib/admin/static/admin/img/tool-right.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tool-right_over.gif b/django/contrib/admin/static/admin/img/tool-right_over.gif Binary files differdeleted file mode 100644 index 4db977e838..0000000000 --- a/django/contrib/admin/static/admin/img/tool-right_over.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tooltag-add.gif b/django/contrib/admin/static/admin/img/tooltag-add.gif Binary files differdeleted file mode 100644 index 8b53d49ae5..0000000000 --- a/django/contrib/admin/static/admin/img/tooltag-add.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tooltag-add.png b/django/contrib/admin/static/admin/img/tooltag-add.png Binary files differnew file mode 100644 index 0000000000..f352cf590f --- /dev/null +++ b/django/contrib/admin/static/admin/img/tooltag-add.png diff --git a/django/contrib/admin/static/admin/img/tooltag-add_over.gif b/django/contrib/admin/static/admin/img/tooltag-add_over.gif Binary files differdeleted file mode 100644 index bfc52f10de..0000000000 --- a/django/contrib/admin/static/admin/img/tooltag-add_over.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright.gif b/django/contrib/admin/static/admin/img/tooltag-arrowright.gif Binary files differdeleted file mode 100644 index cdaaae77ed..0000000000 --- a/django/contrib/admin/static/admin/img/tooltag-arrowright.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright.png b/django/contrib/admin/static/admin/img/tooltag-arrowright.png Binary files differnew file mode 100644 index 0000000000..ac1ac5b89a --- /dev/null +++ b/django/contrib/admin/static/admin/img/tooltag-arrowright.png diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif b/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif Binary files differdeleted file mode 100644 index 7163189604..0000000000 --- a/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif +++ /dev/null diff --git a/django/contrib/admin/static/admin/js/collapse.min.js b/django/contrib/admin/static/admin/js/collapse.min.js index b32fbc3472..0a8c20ea44 100644 --- a/django/contrib/admin/static/admin/js/collapse.min.js +++ b/django/contrib/admin/static/admin/js/collapse.min.js @@ -1,2 +1,2 @@ -(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){0==a(b).find("div.errors").length&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", -[a(this).attr("id")]);return!1})})})(django.jQuery); +(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){a(b).find("div.errors").length==0&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", +[a(this).attr("id")]);return false})})})(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js index b89aedd4cc..cc888a5628 100644 --- a/django/contrib/admin/static/admin/js/inlines.min.js +++ b/django/contrib/admin/static/admin/js/inlines.min.js @@ -1,9 +1,9 @@ -(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),f=""===e.val()||0<e.val()-f.val();c.each(function(){b(this).not("."+ -a.emptyCssClass).addClass(a.formCssClass)});if(c.length&&f){var h;"TR"==c.prop("tagName")?(c=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+ -"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){i(this, -a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c<f;c++){i(b(d).get(c),a.prefix,c);b(d.get(c)).find("*").each(function(){i(this, -a.prefix,c)})}});a.added&&a.added(c)})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),c=function(){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+ -d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!= -typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),c=function(){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix, -addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".form-row .field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(), -DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a}})(django.jQuery); +(function(a){a.fn.formset=function(g){var b=a.extend({},a.fn.formset.defaults,g),i=a(this);g=i.parent();var m=function(e,k,h){var j=RegExp("("+k+"-(\\d+|__prefix__))");k=k+"-"+h;a(e).prop("for")&&a(e).prop("for",a(e).prop("for").replace(j,k));if(e.id)e.id=e.id.replace(j,k);if(e.name)e.name=e.name.replace(j,k)},l=a("#id_"+b.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),d=parseInt(l.val(),10),c=a("#id_"+b.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off");l=c.val()===""||c.val()-l.val()>0;i.each(function(){a(this).not("."+ +b.emptyCssClass).addClass(b.formCssClass)});if(i.length&&l){var f;if(i.prop("tagName")=="TR"){i=this.eq(-1).children().length;g.append('<tr class="'+b.addCssClass+'"><td colspan="'+i+'"><a href="javascript:void(0)">'+b.addText+"</a></tr>");f=g.find("tr:last a")}else{i.filter(":last").after('<div class="'+b.addCssClass+'"><a href="javascript:void(0)">'+b.addText+"</a></div>");f=i.filter(":last").next().find("a")}f.click(function(e){e.preventDefault();var k=a("#id_"+b.prefix+"-TOTAL_FORMS");e=a("#"+ +b.prefix+"-empty");var h=e.clone(true);h.removeClass(b.emptyCssClass).addClass(b.formCssClass).attr("id",b.prefix+"-"+d);if(h.is("tr"))h.children(":last").append('<div><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></div>");else h.is("ul")||h.is("ol")?h.append('<li><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></li>"):h.children(":first").append('<span><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></span>"); +h.find("*").each(function(){m(this,b.prefix,k.val())});h.insertBefore(a(e));a(k).val(parseInt(k.val(),10)+1);d+=1;c.val()!==""&&c.val()-k.val()<=0&&f.parent().hide();h.find("a."+b.deleteCssClass).click(function(j){j.preventDefault();j=a(this).parents("."+b.formCssClass);j.remove();d-=1;b.removed&&b.removed(j);j=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(j.length);if(c.val()===""||c.val()-j.length>0)f.parent().show();for(var n=0,o=j.length;n<o;n++){m(a(j).get(n),b.prefix,n);a(j.get(n)).find("*").each(function(){m(this, +b.prefix,n)})}});b.added&&b.added(h)})}return this};a.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};a.fn.tabularFormset=function(g){var b=a(this),i=function(){a(b.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},m=function(){if(typeof SelectFilter!="undefined"){a(".selectfilter").each(function(d, +c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".field-"+h).find("input, select, textarea").attr("id"))});e.length&&c.prepopulate(e,c.attr("maxlength"))})}; +b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b};a.fn.stackedFormset=function(g){var b=a(this),i=function(){a(b.selector).find(".inline_label").each(function(d){d=d+1;a(this).html(a(this).html().replace(/(#\d+)/g,"#"+d))})},m=function(){if(typeof SelectFilter!= +"undefined"){a(".selectfilter").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".form-row .field-"+h).find("input, select, textarea").attr("id"))}); +e.length&&c.prepopulate(e,c.attr("maxlength"))})};b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b}})(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/prepopulate.js b/django/contrib/admin/static/admin/js/prepopulate.js index 24f24f95a8..db7903a5ef 100644 --- a/django/contrib/admin/static/admin/js/prepopulate.js +++ b/django/contrib/admin/static/admin/js/prepopulate.js @@ -3,32 +3,37 @@ /* Depends on urlify.js Populates a selected field with the values of the dependent fields, - URLifies and shortens the string. - dependencies - array of dependent fields id's - maxLength - maximum length of the URLify'd string + URLifies and shortens the string. + dependencies - array of dependent fields ids + maxLength - maximum length of the URLify'd string */ return this.each(function() { - var field = $(this); - - field.data('_changed', false); - field.change(function() { - field.data('_changed', true); - }); + var prepopulatedField = $(this); var populate = function () { - // Bail if the fields value has changed - if (field.data('_changed') == true) return; - + // Bail if the field's value has been changed by the user + if (prepopulatedField.data('_changed')) { + return; + } + var values = []; $.each(dependencies, function(i, field) { - if ($(field).val().length > 0) { - values.push($(field).val()); - } - }) - field.val(URLify(values.join(' '), maxLength)); + field = $(field); + if (field.val().length > 0) { + values.push(field.val()); + } + }); + prepopulatedField.val(URLify(values.join(' '), maxLength)); }; - $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); + prepopulatedField.data('_changed', false); + prepopulatedField.change(function() { + prepopulatedField.data('_changed', true); + }); + + if (!prepopulatedField.val()) { + $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); + } }); }; })(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/prepopulate.min.js b/django/contrib/admin/static/admin/js/prepopulate.min.js index aa94937963..4a75827c97 100644 --- a/django/contrib/admin/static/admin/js/prepopulate.min.js +++ b/django/contrib/admin/static/admin/js/prepopulate.min.js @@ -1 +1 @@ -(function(a){a.fn.prepopulate=function(d,g){return this.each(function(){var b=a(this);b.data("_changed",false);b.change(function(){b.data("_changed",true)});var c=function(){if(b.data("_changed")!=true){var e=[];a.each(d,function(h,f){a(f).val().length>0&&e.push(a(f).val())});b.val(URLify(e.join(" "),g))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery); +(function(b){b.fn.prepopulate=function(e,g){return this.each(function(){var a=b(this),d=function(){if(!a.data("_changed")){var f=[];b.each(e,function(h,c){c=b(c);c.val().length>0&&f.push(c.val())});a.val(URLify(f.join(" "),g))}};a.data("_changed",false);a.change(function(){a.data("_changed",true)});a.val()||b(e.join(",")).keyup(d).change(d).focus(d)})}})(django.jQuery); diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html index 1c928a0d4d..3e557ebef3 100644 --- a/django/contrib/admin/templates/registration/password_change_done.html +++ b/django/contrib/admin/templates/registration/password_change_done.html @@ -8,12 +8,8 @@ </div> {% endblock %} -{% block title %}{% trans 'Password change successful' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %} - -<h1>{% trans 'Password change successful' %}</h1> - <p>{% trans 'Your password was changed.' %}</p> - {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index f7316a739f..921df0ac72 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -9,7 +9,8 @@ </div> {% endblock %} -{% block title %}{% trans 'Password change' %}{% endblock %} +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %}<div id="content-main"> @@ -21,7 +22,6 @@ </p> {% endif %} -<h1>{% trans 'Password change' %}</h1> <p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p> diff --git a/django/contrib/admin/templates/registration/password_reset_complete.html b/django/contrib/admin/templates/registration/password_reset_complete.html index d97f338197..19f87a5b76 100644 --- a/django/contrib/admin/templates/registration/password_reset_complete.html +++ b/django/contrib/admin/templates/registration/password_reset_complete.html @@ -8,12 +8,11 @@ </div> {% endblock %} -{% block title %}{% trans 'Password reset complete' %}{% endblock %} +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %} -<h1>{% trans 'Password reset complete' %}</h1> - <p>{% trans "Your password has been set. You may go ahead and log in now." %}</p> <p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p> diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index 81020b929f..bd24806d46 100644 --- a/django/contrib/admin/templates/registration/password_reset_confirm.html +++ b/django/contrib/admin/templates/registration/password_reset_confirm.html @@ -8,14 +8,12 @@ </div> {% endblock %} -{% block title %}{% trans 'Password reset' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %} {% if validlink %} -<h1>{% trans 'Enter new password' %}</h1> - <p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p> <form action="" method="post">{% csrf_token %} @@ -28,8 +26,6 @@ {% else %} -<h1>{% trans 'Password reset unsuccessful' %}</h1> - <p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p> {% endif %} diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index 98471041b5..d157306a91 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -8,12 +8,10 @@ </div> {% endblock %} -{% block title %}{% trans 'Password reset successful' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %} -<h1>{% trans 'Password reset successful' %}</h1> - <p>{% trans "We've emailed you instructions for setting your password. You should be receiving them shortly." %}</p> <p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p> diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index c9998a1a3b..dc05cd0245 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -8,12 +8,10 @@ </div> {% endblock %} -{% block title %}{% trans "Password reset" %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}<h1>{{ title }}</h1>{% endblock %} {% block content %} -<h1>{% trans "Password reset" %}</h1> - <p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p> <form action="" method="post">{% csrf_token %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 8596dfb825..6c3c3e8511 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -180,7 +180,7 @@ def items_for_result(cl, result, form): first = True pk = cl.lookup_opts.pk.attname for field_name in cl.list_display: - row_class = '' + row_classes = ['field-%s' % field_name] try: f, attr, value = lookup_field(field_name, result, cl.model_admin) except ObjectDoesNotExist: @@ -188,7 +188,7 @@ def items_for_result(cl, result, form): else: if f is None: if field_name == 'action_checkbox': - row_class = mark_safe(' class="action-checkbox"') + row_classes = ['action-checkbox'] allow_tags = getattr(attr, 'allow_tags', False) boolean = getattr(attr, 'boolean', False) if boolean: @@ -199,7 +199,7 @@ def items_for_result(cl, result, form): if allow_tags: result_repr = mark_safe(result_repr) if isinstance(value, (datetime.date, datetime.time)): - row_class = mark_safe(' class="nowrap"') + row_classes.append('nowrap') else: if isinstance(f.rel, models.ManyToOneRel): field_val = getattr(result, f.name) @@ -210,9 +210,10 @@ def items_for_result(cl, result, form): else: result_repr = display_for_field(value, f) if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)): - row_class = mark_safe(' class="nowrap"') + row_classes.append('nowrap') if force_text(result_repr) == '': result_repr = mark_safe(' ') + row_class = mark_safe(' class="%s"' % ' '.join(row_classes)) # If list_display_links not defined, add the link tag to the first field if (first and not cl.list_display_links) or field_name in cl.list_display_links: table_tag = {True:'th', False:'td'}[first] diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 98ac1a657e..005bde3023 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -9,7 +9,7 @@ def prepopulated_fields_js(context): the prepopulated fields for both the admin form and inlines. """ prepopulated_fields = [] - if context['add'] and 'adminform' in context: + if 'adminform' in context: prepopulated_fields.extend(context['adminform'].prepopulated_fields) if 'inline_admin_formsets' in context: for inline_admin_formset in context['inline_admin_formsets']: diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index f766031bf8..3325747a9f 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import sys import warnings @@ -7,7 +8,6 @@ from django.core.urlresolvers import reverse from django.db import models from django.db.models.fields import FieldDoesNotExist from django.utils import six -from django.utils.datastructures import SortedDict from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext, ugettext_lazy @@ -319,13 +319,13 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): def get_ordering_field_columns(self): """ - Returns a SortedDict of ordering field column numbers and asc/desc + Returns an OrderedDict of ordering field column numbers and asc/desc """ # We must cope with more than one column having the same underlying sort # field, so we base things on column numbers. ordering = self._get_default_ordering() - ordering_fields = SortedDict() + ordering_fields = OrderedDict() if ORDER_VAR not in self.params: # for ordering specified on ModelAdmin or model Meta, we don't know # the right column numbers absolutely, because there might be more diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 4b79401dbc..c4b15cdd6a 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -116,6 +116,8 @@ def url_params_from_lookup_dict(lookups): if lookups and hasattr(lookups, 'items'): items = [] for k, v in lookups.items(): + if callable(v): + v = v() if isinstance(v, (tuple, list)): v = ','.join([str(x) for x in v]) elif isinstance(v, bool): @@ -285,7 +287,14 @@ class AdminTextInputWidget(forms.TextInput): final_attrs.update(attrs) super(AdminTextInputWidget, self).__init__(attrs=final_attrs) -class AdminURLFieldWidget(forms.TextInput): +class AdminEmailInputWidget(forms.EmailInput): + def __init__(self, attrs=None): + final_attrs = {'class': 'vTextField'} + if attrs is not None: + final_attrs.update(attrs) + super(AdminEmailInputWidget, self).__init__(attrs=final_attrs) + +class AdminURLFieldWidget(forms.URLInput): def __init__(self, attrs=None): final_attrs = {'class': 'vURLField'} if attrs is not None: diff --git a/django/contrib/admindocs/tests/test_fields.py b/django/contrib/admindocs/tests/test_fields.py index b505d2deeb..e8fe0b0caa 100644 --- a/django/contrib/admindocs/tests/test_fields.py +++ b/django/contrib/admindocs/tests/test_fields.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import unittest diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index b3faf06e25..5515a3eee4 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -1,3 +1,4 @@ +from importlib import import_module import inspect import os import re @@ -13,7 +14,6 @@ from django.http import Http404 from django.core import urlresolvers from django.contrib.admindocs import utils from django.contrib.sites.models import Site -from django.utils.importlib import import_module from django.utils._os import upath from django.utils import six from django.utils.translation import ugettext as _ @@ -319,7 +319,7 @@ def load_all_installed_template_libraries(): libraries = [] for library_name in libraries: try: - lib = template.get_library(library_name) + template.get_library(library_name) except template.InvalidTemplateLibrary: pass diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index ca660606e5..ff08f41798 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters csrf_protect_m = method_decorator(csrf_protect) +sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) class GroupAdmin(admin.ModelAdmin): @@ -87,7 +88,7 @@ class UserAdmin(admin.ModelAdmin): return False return super(UserAdmin, self).lookup_allowed(lookup, value) - @sensitive_post_parameters() + @sensitive_post_parameters_m @csrf_protect_m @transaction.atomic def add_view(self, request, form_url='', extra_context=None): @@ -118,7 +119,7 @@ class UserAdmin(admin.ModelAdmin): return super(UserAdmin, self).add_view(request, form_url, extra_context) - @sensitive_post_parameters() + @sensitive_post_parameters_m def user_change_password(self, request, id, form_url=''): if not self.has_change_permission(request): raise PermissionDenied @@ -127,6 +128,8 @@ class UserAdmin(admin.ModelAdmin): form = self.change_password_form(user, request.POST) if form.is_valid(): form.save() + change_message = self.construct_change_message(request, form, None) + self.log_change(request, request.user, change_message) msg = ugettext('Password changed successfully.') messages.success(request, msg) return HttpResponseRedirect('..') diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 6b31f72b03..cb79291c17 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -17,7 +17,9 @@ class ModelBackend(object): if user.check_password(password): return user except UserModel.DoesNotExist: - return None + # Run the default password hasher once to reduce the timing + # difference between an existing and a non-existing user (#20760). + UserModel().set_password(password) def get_group_permissions(self, user_obj, obj=None): """ diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 2d7a7c14d4..3eba8abd51 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from collections import OrderedDict + from django import forms from django.forms.util import flatatt from django.template import loader -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes from django.utils.html import format_html, format_html_join from django.utils.http import urlsafe_base64_encode @@ -191,13 +192,28 @@ class AuthenticationForm(forms.Form): code='invalid_login', params={'username': self.username_field.verbose_name}, ) - elif not self.user_cache.is_active: - raise forms.ValidationError( - self.error_messages['inactive'], - code='inactive', - ) + else: + self.confirm_login_allowed(self.user_cache) + return self.cleaned_data + def confirm_login_allowed(self, user): + """ + Controls whether the given User may log in. This is a policy setting, + independent of end-user authentication. This default behavior is to + allow login by active users, and reject login by inactive users. + + If the given user cannot log in, this method should raise a + ``forms.ValidationError``. + + If the given user may log in, this method should return None. + """ + if not user.is_active: + raise forms.ValidationError( + self.error_messages['inactive'], + code='inactive', + ) + def get_user_id(self): if self.user_cache: return self.user_cache.id @@ -214,7 +230,7 @@ class PasswordResetForm(forms.Form): subject_template_name='registration/password_reset_subject.txt', email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, - from_email=None, request=None): + from_email=None, request=None, html_email_template_name=None): """ Generates a one-use only link for resetting password and sends to the user. @@ -247,7 +263,12 @@ class PasswordResetForm(forms.Form): # Email subject *must not* contain newlines subject = ''.join(subject.splitlines()) email = loader.render_to_string(email_template_name, c) - send_mail(subject, email, from_email, [user.email]) + + if html_email_template_name: + html_email = loader.render_to_string(html_email_template_name, c) + else: + html_email = None + send_mail(subject, email, from_email, [user.email], html_message=html_email) class SetPasswordForm(forms.Form): @@ -309,7 +330,7 @@ class PasswordChangeForm(SetPasswordForm): ) return old_password -PasswordChangeForm.base_fields = SortedDict([ +PasswordChangeForm.base_fields = OrderedDict([ (k, PasswordChangeForm.base_fields[k]) for k in ['old_password', 'new_password1', 'new_password2'] ]) @@ -350,3 +371,11 @@ class AdminPasswordChangeForm(forms.Form): if commit: self.user.save() return self.user + + def _get_changed_data(self): + data = super(AdminPasswordChangeForm, self).changed_data + for name in self.fields.keys(): + if name not in data: + return [] + return ['password'] + changed_data = property(_get_changed_data) diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py index 7656b50437..c4dca38252 100644 --- a/django/contrib/auth/hashers.py +++ b/django/contrib/auth/hashers.py @@ -2,13 +2,13 @@ from __future__ import unicode_literals import base64 import binascii +from collections import OrderedDict import hashlib +import importlib from django.dispatch import receiver from django.conf import settings from django.test.signals import setting_changed -from django.utils import importlib -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes, force_str, force_text from django.core.exceptions import ImproperlyConfigured from django.utils.crypto import ( @@ -172,7 +172,7 @@ class BasePasswordHasher(object): if isinstance(self.library, (tuple, list)): name, mod_path = self.library else: - name = mod_path = self.library + mod_path = self.library try: module = importlib.import_module(mod_path) except ImportError as e: @@ -243,7 +243,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, iterations, salt, hash = encoded.split('$', 3) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('iterations'), iterations), (_('salt'), mask_hash(salt)), @@ -320,7 +320,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher): algorithm, empty, algostr, work_factor, data = encoded.split('$', 4) assert algorithm == self.algorithm salt, checksum = data[:22], data[22:] - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('work factor'), work_factor), (_('salt'), mask_hash(salt)), @@ -368,7 +368,7 @@ class SHA1PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, hash = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), mask_hash(salt, show=2)), (_('hash'), mask_hash(hash)), @@ -396,7 +396,7 @@ class MD5PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, hash = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), mask_hash(salt, show=2)), (_('hash'), mask_hash(hash)), @@ -429,7 +429,7 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): assert encoded.startswith('sha1$$') hash = encoded[6:] - return SortedDict([ + return OrderedDict([ (_('algorithm'), self.algorithm), (_('hash'), mask_hash(hash)), ]) @@ -462,7 +462,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher): return constant_time_compare(encoded, encoded_2) def safe_summary(self, encoded): - return SortedDict([ + return OrderedDict([ (_('algorithm'), self.algorithm), (_('hash'), mask_hash(encoded, show=3)), ]) @@ -496,7 +496,7 @@ class CryptPasswordHasher(BasePasswordHasher): def safe_summary(self, encoded): algorithm, salt, data = encoded.split('$', 2) assert algorithm == self.algorithm - return SortedDict([ + return OrderedDict([ (_('algorithm'), algorithm), (_('salt'), salt), (_('hash'), mask_hash(data, show=3)), diff --git a/django/contrib/auth/tests/templates/registration/html_password_reset_email.html b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html new file mode 100644 index 0000000000..1ebb550048 --- /dev/null +++ b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html @@ -0,0 +1 @@ +<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html> diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py index fc5a80e8dd..4e83d786cf 100644 --- a/django/contrib/auth/tests/test_auth_backends.py +++ b/django/contrib/auth/tests/test_auth_backends.py @@ -12,6 +12,17 @@ from django.contrib.auth import authenticate, get_user from django.http import HttpRequest from django.test import TestCase from django.test.utils import override_settings +from django.contrib.auth.hashers import MD5PasswordHasher + + +class CountingMD5PasswordHasher(MD5PasswordHasher): + """Hasher that counts how many times it computes a hash.""" + + calls = 0 + + def encode(self, *args, **kwargs): + type(self).calls += 1 + return super(CountingMD5PasswordHasher, self).encode(*args, **kwargs) class BaseModelBackendTest(object): @@ -107,10 +118,26 @@ class BaseModelBackendTest(object): self.assertEqual(user.get_all_permissions(), set(['auth.test'])) def test_get_all_superuser_permissions(self): - "A superuser has all permissions. Refs #14795" + """A superuser has all permissions. Refs #14795.""" user = self.UserModel._default_manager.get(pk=self.superuser.pk) self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.tests.test_auth_backends.CountingMD5PasswordHasher',)) + def test_authentication_timing(self): + """Hasher is run once regardless of whether the user exists. Refs #20760.""" + # Re-set the password, because this tests overrides PASSWORD_HASHERS + self.user.set_password('test') + self.user.save() + + CountingMD5PasswordHasher.calls = 0 + username = getattr(self.user, self.UserModel.USERNAME_FIELD) + authenticate(username=username, password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + + CountingMD5PasswordHasher.calls = 0 + authenticate(username='no_such_user', password='test') + self.assertEqual(CountingMD5PasswordHasher.calls, 1) + @skipIfCustomUser class ModelBackendTest(BaseModelBackendTest, TestCase): diff --git a/django/contrib/auth/tests/test_context_processors.py b/django/contrib/auth/tests/test_context_processors.py index 9e56cfce85..d1d912eb15 100644 --- a/django/contrib/auth/tests/test_context_processors.py +++ b/django/contrib/auth/tests/test_context_processors.py @@ -161,7 +161,7 @@ class AuthContextProcessorTests(TestCase): # Exception RuntimeError: 'maximum recursion depth exceeded while # calling a Python object' in <type 'exceptions.AttributeError'> # ignored" - query = Q(user=response.context['user']) & Q(someflag=True) + Q(user=response.context['user']) & Q(someflag=True) # Tests for user equality. This is hard because User defines # equality in a non-duck-typing way diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py index 0b998105af..eef366f184 100644 --- a/django/contrib/auth/tests/test_forms.py +++ b/django/contrib/auth/tests/test_forms.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals import os +import re +from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, @@ -131,6 +133,40 @@ class AuthenticationFormTest(TestCase): self.assertEqual(form.non_field_errors(), [force_text(form.error_messages['inactive'])]) + def test_custom_login_allowed_policy(self): + # The user is inactive, but our custom form policy allows him to log in. + data = { + 'username': 'inactive', + 'password': 'password', + } + + class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm): + def confirm_login_allowed(self, user): + pass + + form = AuthenticationFormWithInactiveUsersOkay(None, data) + self.assertTrue(form.is_valid()) + + # If we want to disallow some logins according to custom logic, + # we should raise a django.forms.ValidationError in the form. + class PickyAuthenticationForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if user.username == "inactive": + raise forms.ValidationError(_("This user is disallowed.")) + raise forms.ValidationError(_("Sorry, nobody's allowed in.")) + + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ['This user is disallowed.']) + + data = { + 'username': 'testclient', + 'password': 'password', + } + form = PickyAuthenticationForm(None, data) + self.assertFalse(form.is_valid()) + self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."]) + def test_success(self): # The success case data = { @@ -272,7 +308,7 @@ class UserChangeFormTest(TestCase): fields = ('groups',) # Just check we can create it - form = MyUserForm({}) + MyUserForm({}) def test_unsuable_password(self): user = User.objects.get(username='empty_password') @@ -417,6 +453,60 @@ class PasswordResetFormTest(TestCase): form.save() self.assertEqual(len(mail.outbox), 0) + @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), + ) + def test_save_plaintext_email(self): + """ + Test the PasswordResetForm.save() method with no html_email_template_name + parameter passed in. + Test to ensure original behavior is unchanged after the parameter was added. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertFalse(message.is_multipart()) + self.assertEqual(message.get_content_type(), 'text/plain') + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(mail.outbox[0].alternatives), 0) + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload())) + + @override_settings( + TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(upath(__file__)), 'templates'), + ), + ) + def test_save_html_email_template_name(self): + """ + Test the PasswordResetFOrm.save() method with html_email_template_name + parameter specified. + Test to ensure that a multipart email is sent with both text/plain + and text/html parts. + """ + (user, username, email) = self.create_dummy_user() + form = PasswordResetForm({"email": email}) + self.assertTrue(form.is_valid()) + form.save(html_email_template_name='registration/html_password_reset_email.html') + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(len(mail.outbox[0].alternatives), 1) + message = mail.outbox[0].message() + self.assertEqual(message.get('subject'), 'Custom password reset on example.com') + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertEqual(message.get_all('to'), [email]) + self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload())) + self.assertTrue(re.match(r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$', message.get_payload(1).get_payload())) + class ReadOnlyPasswordHashTest(TestCase): diff --git a/django/contrib/auth/tests/test_templates.py b/django/contrib/auth/tests/test_templates.py new file mode 100644 index 0000000000..f7d74748a2 --- /dev/null +++ b/django/contrib/auth/tests/test_templates.py @@ -0,0 +1,59 @@ +from django.contrib.auth import authenticate +from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.auth.views import ( + password_reset, password_reset_done, password_reset_confirm, + password_reset_complete, password_change, password_change_done, +) +from django.test import RequestFactory, TestCase +from django.test.utils import override_settings +from django.utils.encoding import force_bytes, force_text +from django.utils.http import urlsafe_base64_encode + + +@skipIfCustomUser +@override_settings( + PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), +) +class AuthTemplateTests(TestCase): + + def test_titles(self): + rf = RequestFactory() + user = User.objects.create_user('jsmith', 'jsmith@example.com', 'pass') + user = authenticate(username=user.username, password='pass') + request = rf.get('/somepath/') + request.user = user + + response = password_reset(request, post_reset_redirect='dummy/') + self.assertContains(response, '<title>Password reset</title>') + self.assertContains(response, '<h1>Password reset</h1>') + + response = password_reset_done(request) + self.assertContains(response, '<title>Password reset successful</title>') + self.assertContains(response, '<h1>Password reset successful</h1>') + + # password_reset_confirm invalid token + response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/') + self.assertContains(response, '<title>Password reset unsuccessful</title>') + self.assertContains(response, '<h1>Password reset unsuccessful</h1>') + + # password_reset_confirm valid token + default_token_generator = PasswordResetTokenGenerator() + token = default_token_generator.make_token(user) + uidb64 = force_text(urlsafe_base64_encode(force_bytes(user.pk))) + response = password_reset_confirm(request, uidb64, token, post_reset_redirect='dummy/') + self.assertContains(response, '<title>Enter new password</title>') + self.assertContains(response, '<h1>Enter new password</h1>') + + response = password_reset_complete(request) + self.assertContains(response, '<title>Password reset complete</title>') + self.assertContains(response, '<h1>Password reset complete</h1>') + + response = password_change(request, post_change_redirect='dummy/') + self.assertContains(response, '<title>Password change</title>') + self.assertContains(response, '<h1>Password change</h1>') + + response = password_change_done(request) + self.assertContains(response, '<title>Password change successful</title>') + self.assertContains(response, '<h1>Password change successful</h1>') diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index b939dff058..22ccbfd225 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -8,6 +8,7 @@ except ImportError: # Python 2 from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite +from django.contrib.admin.models import LogEntry from django.contrib.auth.models import User from django.core import mail from django.core.urlresolvers import reverse, NoReverseMatch @@ -54,6 +55,11 @@ class AuthViewsTestCase(TestCase): self.assertTrue(SESSION_KEY in self.client.session) return response + def logout(self): + response = self.client.get('/admin/logout/') + self.assertEqual(response.status_code, 200) + self.assertTrue(SESSION_KEY not in self.client.session) + def assertFormError(self, response, error): """Assert that error is found in response.context['form'] errors""" form_errors = list(itertools.chain(*response.context['form'].errors.values())) @@ -122,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase): self.assertEqual(len(mail.outbox), 1) self.assertTrue("http://" in mail.outbox[0].body) self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) + # optional multipart text/html email has been added. Make sure original, + # default functionality is 100% the same + self.assertFalse(mail.outbox[0].message().is_multipart()) + + def test_html_mail_template(self): + """ + A multipart email with text/plain and text/html is sent + if the html_email_template parameter is passed to the view + """ + response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0].message() + self.assertEqual(len(message.get_payload()), 2) + self.assertTrue(message.is_multipart()) + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + self.assertTrue('<html>' not in message.get_payload(0).get_payload()) + self.assertTrue('<html>' in message.get_payload(1).get_payload()) def test_email_found_custom_from(self): "Email is sent if a valid email address is provided for password reset when a custom from_email is provided." @@ -178,7 +203,7 @@ class PasswordResetTest(AuthViewsTestCase): def _test_confirm_start(self): # Start by creating the email - response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) self.assertEqual(len(mail.outbox), 1) return self._read_signup_email(mail.outbox[0]) @@ -322,7 +347,7 @@ class ChangePasswordTest(AuthViewsTestCase): }) def logout(self): - response = self.client.get('/logout/') + self.client.get('/logout/') def test_password_change_fails_with_invalid_old_password(self): self.login() @@ -344,7 +369,7 @@ class ChangePasswordTest(AuthViewsTestCase): def test_password_change_succeeds(self): self.login() - response = self.client.post('/password_change/', { + self.client.post('/password_change/', { 'old_password': 'password', 'new_password1': 'password1', 'new_password2': 'password1', @@ -459,7 +484,7 @@ class LoginTest(AuthViewsTestCase): def test_login_form_contains_request(self): # 15198 - response = self.client.post('/custom_requestauth_login/', { + self.client.post('/custom_requestauth_login/', { 'username': 'testclient', 'password': 'password', }, follow=True) @@ -670,18 +695,70 @@ class LogoutTest(AuthViewsTestCase): self.confirm_logged_out() @skipIfCustomUser +@override_settings( + PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), +) class ChangelistTests(AuthViewsTestCase): urls = 'django.contrib.auth.tests.urls_admin' - # #20078 - users shouldn't be allowed to guess password hashes via - # repeated password__startswith queries. - def test_changelist_disallows_password_lookups(self): - # Make me a superuser before loging in. + def setUp(self): + # Make me a superuser before logging in. User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True) self.login() + self.admin = User.objects.get(pk=1) + def get_user_data(self, user): + return { + 'username': user.username, + 'password': user.password, + 'email': user.email, + 'is_active': user.is_active, + 'is_staff': user.is_staff, + 'is_superuser': user.is_superuser, + 'last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'last_login_1': user.last_login.strftime('%H:%M:%S'), + 'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'), + 'initial-last_login_1': user.last_login.strftime('%H:%M:%S'), + 'date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'), + 'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'), + 'first_name': user.first_name, + 'last_name': user.last_name, + } + + # #20078 - users shouldn't be allowed to guess password hashes via + # repeated password__startswith queries. + def test_changelist_disallows_password_lookups(self): # A lookup that tries to filter on password isn't OK with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: response = self.client.get('/admin/auth/user/?password__startswith=sha1$') self.assertEqual(response.status_code, 400) self.assertEqual(len(logger_calls), 1) + + def test_user_change_email(self): + data = self.get_user_data(self.admin) + data['email'] = 'new_' + data['email'] + response = self.client.post('/admin/auth/user/%s/' % self.admin.pk, data) + self.assertRedirects(response, '/admin/auth/user/') + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed email.') + + def test_user_not_change(self): + response = self.client.post('/admin/auth/user/%s/' % self.admin.pk, + self.get_user_data(self.admin) + ) + self.assertRedirects(response, '/admin/auth/user/') + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'No fields changed.') + + def test_user_change_password(self): + response = self.client.post('/admin/auth/user/%s/password/' % self.admin.pk, { + 'password1': 'password1', + 'password2': 'password1', + }) + self.assertRedirects(response, '/admin/auth/user/%s/' % self.admin.pk) + row = LogEntry.objects.latest('id') + self.assertEqual(row.change_message, 'Changed password.') + self.logout() + self.login(password='password1') diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index 502fc659d4..2af83d21ea 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -67,6 +67,7 @@ urlpatterns = urlpatterns + patterns('', (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')), (r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')), + (r'^password_reset/html_email_template/$', 'django.contrib.auth.views.password_reset', dict(html_email_template_name='registration/html_password_reset_email.html')), (r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', dict(post_reset_redirect='/custom/')), diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index b731a2b3d1..d852106a4e 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -7,10 +7,9 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.template.response import TemplateResponse -from django.utils.http import base36_to_int, is_safe_url, urlsafe_base64_decode, urlsafe_base64_encode +from django.utils.http import is_safe_url, urlsafe_base64_decode from django.utils.translation import ugettext as _ from django.shortcuts import resolve_url -from django.utils.encoding import force_bytes, force_text from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect @@ -141,7 +140,8 @@ def password_reset(request, is_admin_site=False, post_reset_redirect=None, from_email=None, current_app=None, - extra_context=None): + extra_context=None, + html_email_template_name=None): if post_reset_redirect is None: post_reset_redirect = reverse('password_reset_done') else: @@ -156,6 +156,7 @@ def password_reset(request, is_admin_site=False, 'email_template_name': email_template_name, 'subject_template_name': subject_template_name, 'request': request, + 'html_email_template_name': html_email_template_name, } if is_admin_site: opts = dict(opts, domain_override=request.get_host()) @@ -165,6 +166,7 @@ def password_reset(request, is_admin_site=False, form = password_reset_form() context = { 'form': form, + 'title': _('Password reset'), } if extra_context is not None: context.update(extra_context) @@ -175,7 +177,9 @@ def password_reset(request, is_admin_site=False, def password_reset_done(request, template_name='registration/password_reset_done.html', current_app=None, extra_context=None): - context = {} + context = { + 'title': _('Password reset successful'), + } if extra_context is not None: context.update(extra_context) return TemplateResponse(request, template_name, context, @@ -209,6 +213,7 @@ def password_reset_confirm(request, uidb64=None, token=None, if user is not None and token_generator.check_token(user, token): validlink = True + title = _('Enter new password') if request.method == 'POST': form = set_password_form(user, request.POST) if form.is_valid(): @@ -219,8 +224,10 @@ def password_reset_confirm(request, uidb64=None, token=None, else: validlink = False form = None + title = _('Password reset unsuccessful') context = { 'form': form, + 'title': title, 'validlink': validlink, } if extra_context is not None: @@ -232,7 +239,8 @@ def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): context = { - 'login_url': resolve_url(settings.LOGIN_URL) + 'login_url': resolve_url(settings.LOGIN_URL), + 'title': _('Password reset complete'), } if extra_context is not None: context.update(extra_context) @@ -261,6 +269,7 @@ def password_change(request, form = password_change_form(user=request.user) context = { 'form': form, + 'title': _('Password change'), } if extra_context is not None: context.update(extra_context) @@ -272,7 +281,9 @@ def password_change(request, def password_change_done(request, template_name='registration/password_change_done.html', current_app=None, extra_context=None): - context = {} + context = { + 'title': _('Password change successful'), + } if extra_context is not None: context.update(extra_context) return TemplateResponse(request, template_name, context, diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 0b3fcebc51..56ed32bafe 100644 --- a/django/contrib/comments/__init__.py +++ b/django/contrib/comments/__init__.py @@ -1,10 +1,10 @@ +from importlib import import_module import warnings from django.conf import settings from django.core import urlresolvers from django.core.exceptions import ImproperlyConfigured from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm -from django.utils.importlib import import_module warnings.warn("django.contrib.comments is deprecated and will be removed before Django 1.8.", DeprecationWarning) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index befd326092..a2cbe33c0e 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django import http from django.conf import settings from django.contrib import comments diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index 31bb98fa63..484ceac02c 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django import template from django.conf import settings from django.contrib import comments diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 6f120f82e0..186f39068d 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -465,10 +465,10 @@ class GenericInlineModelAdmin(InlineModelAdmin): formset = BaseGenericInlineFormSet def get_formset(self, request, obj=None, **kwargs): - if self.declared_fieldsets: - fields = flatten_fieldsets(self.declared_fieldsets) + if 'fields' in kwargs: + fields = kwargs.pop('fields') else: - fields = None + fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: diff --git a/django/contrib/flatpages/tests/test_csrf.py b/django/contrib/flatpages/tests/test_csrf.py index cb51c124b8..d8f82b5d43 100644 --- a/django/contrib/flatpages/tests/test_csrf.py +++ b/django/contrib/flatpages/tests/test_csrf.py @@ -15,6 +15,7 @@ from django.test.utils import override_settings 'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', ), + CSRF_FAILURE_VIEW='django.views.csrf.csrf_failure', TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py index d2e6987fa0..53b0da6e8e 100644 --- a/django/contrib/formtools/preview.py +++ b/django/contrib/formtools/preview.py @@ -39,7 +39,7 @@ class FormPreview(object): """ while 1: try: - f = self.form.base_fields[name] + self.form.base_fields[name] except KeyError: break # This field name isn't being used by the form. name += '_' diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py index f96f89ecdf..c86a7408e0 100644 --- a/django/contrib/formtools/tests/urls.py +++ b/django/contrib/formtools/tests/urls.py @@ -2,8 +2,6 @@ This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. """ -from __future__ import absolute_import - from django.conf.urls import patterns, url from django.contrib.formtools.tests.tests import TestFormPreview diff --git a/django/contrib/formtools/tests/wizard/storage.py b/django/contrib/formtools/tests/wizard/storage.py index 17968dfcda..c54ed9a058 100644 --- a/django/contrib/formtools/tests/wizard/storage.py +++ b/django/contrib/formtools/tests/wizard/storage.py @@ -1,8 +1,8 @@ from datetime import datetime +from importlib import import_module from django.http import HttpRequest from django.conf import settings -from django.utils.importlib import import_module from django.contrib.auth.models import User diff --git a/django/contrib/formtools/tests/wizard/test_forms.py b/django/contrib/formtools/tests/wizard/test_forms.py index 21917822a7..ba3fbfdd48 100644 --- a/django/contrib/formtools/tests/wizard/test_forms.py +++ b/django/contrib/formtools/tests/wizard/test_forms.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals +from importlib import import_module + from django import forms, http from django.conf import settings from django.db import models from django.test import TestCase from django.template.response import TemplateResponse -from django.utils.importlib import import_module from django.contrib.auth.models import User diff --git a/django/contrib/formtools/tests/wizard/wizardtests/forms.py b/django/contrib/formtools/tests/wizard/wizardtests/forms.py index 6a8132971b..dbb0e83d6f 100644 --- a/django/contrib/formtools/tests/wizard/wizardtests/forms.py +++ b/django/contrib/formtools/tests/wizard/wizardtests/forms.py @@ -9,10 +9,9 @@ from django.forms.models import modelformset_factory from django.http import HttpResponse from django.template import Template, Context -from django.contrib.auth.models import User - from django.contrib.formtools.wizard.views import WizardView + temp_storage_location = tempfile.mkdtemp(dir=os.environ.get('DJANGO_TEST_TEMP_DIR')) temp_storage = FileSystemStorage(location=temp_storage_location) diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py index c478f20854..9c81f77ed2 100644 --- a/django/contrib/formtools/wizard/views.py +++ b/django/contrib/formtools/wizard/views.py @@ -1,3 +1,4 @@ +from collections import OrderedDict import re from django import forms @@ -5,7 +6,6 @@ from django.shortcuts import redirect from django.core.urlresolvers import reverse 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 @@ -17,7 +17,7 @@ from django.contrib.formtools.wizard.forms import ManagementForm def normalize_name(name): """ - Converts camel-case style names into underscore seperated words. Example:: + Converts camel-case style names into underscore separated words. Example:: >>> normalize_name('oneTwoThree') 'one_two_three' @@ -158,7 +158,7 @@ class WizardView(TemplateView): form_list = form_list or kwargs.pop('form_list', getattr(cls, 'form_list', None)) or [] - computed_form_list = SortedDict() + computed_form_list = OrderedDict() assert len(form_list) > 0, 'at least one form is needed' @@ -206,7 +206,7 @@ class WizardView(TemplateView): The form_list is always generated on the fly because condition methods could use data from other (maybe previous forms). """ - form_list = SortedDict() + form_list = OrderedDict() for form_key, form_class in six.iteritems(self.form_list): # try to fetch the value from condition list, by default, the form # gets passed to the new list. @@ -498,9 +498,10 @@ class WizardView(TemplateView): if step is None: step = self.steps.current form_list = self.get_form_list() - key = form_list.keyOrder.index(step) + 1 - if len(form_list.keyOrder) > key: - return form_list.keyOrder[key] + keys = list(form_list.keys()) + key = keys.index(step) + 1 + if len(keys) > key: + return keys[key] return None def get_prev_step(self, step=None): @@ -512,9 +513,10 @@ class WizardView(TemplateView): if step is None: step = self.steps.current form_list = self.get_form_list() - key = form_list.keyOrder.index(step) - 1 + keys = list(form_list.keys()) + key = keys.index(step) - 1 if key >= 0: - return form_list.keyOrder[key] + return keys[key] return None def get_step_index(self, step=None): @@ -524,7 +526,7 @@ class WizardView(TemplateView): """ if step is None: step = self.steps.current - return self.get_form_list().keyOrder.index(step) + return list(self.get_form_list().keys()).index(step) def get_context_data(self, form, **kwargs): """ diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py index 7e79be1860..3c748b5b80 100644 --- a/django/contrib/gis/admin/options.py +++ b/django/contrib/gis/admin/options.py @@ -55,7 +55,7 @@ class GeoModelAdmin(ModelAdmin): 3D editing). """ if isinstance(db_field, models.GeometryField) and db_field.dim < 3: - request = kwargs.pop('request', None) + kwargs.pop('request', None) # Setting the widget with the newly defined widget. kwargs['widget'] = self.get_map_widget(db_field) return db_field.formfield(**kwargs) diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py index 2f0720ed84..22457dd4de 100644 --- a/django/contrib/gis/db/backends/spatialite/creation.py +++ b/django/contrib/gis/db/backends/spatialite/creation.py @@ -1,10 +1,12 @@ import os + from django.conf import settings from django.core.cache import get_cache from django.core.cache.backends.db import BaseDatabaseCache from django.core.exceptions import ImproperlyConfigured from django.db.backends.sqlite3.creation import DatabaseCreation + class SpatiaLiteCreation(DatabaseCreation): def create_test_db(self, verbosity=1, autoclobber=False): @@ -53,8 +55,6 @@ class SpatiaLiteCreation(DatabaseCreation): interactive=False, database=self.connection.alias) - from django.core.cache import get_cache - from django.core.cache.backends.db import BaseDatabaseCache for cache_alias in settings.CACHES: cache = get_cache(cache_alias) if isinstance(cache, BaseDatabaseCache): @@ -62,7 +62,7 @@ class SpatiaLiteCreation(DatabaseCreation): # Get a cursor (even though we don't need one yet). This has # the side effect of initializing the test database. - cursor = self.connection.cursor() + self.connection.cursor() return test_database_name diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 2e221b7477..d29705986f 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -148,6 +148,7 @@ class GeometryField(Field): value properly, and preserve any other lookup parameters before returning to the caller. """ + value = super(GeometryField, self).get_prep_value(value) if isinstance(value, SQLEvaluator): return value elif isinstance(value, (tuple, list)): diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index d75e8cd288..c5a87f40e0 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -101,7 +101,7 @@ class OGRGeometry(GDALBase): else: # Seeing if the input is a valid short-hand string # (e.g., 'Point', 'POLYGON'). - ogr_t = OGRGeomType(geom_input) + OGRGeomType(geom_input) g = capi.create_geom(OGRGeomType(geom_input).num) elif isinstance(geom_input, memoryview): # WKB was passed in @@ -341,7 +341,7 @@ class OGRGeometry(GDALBase): sz = self.wkb_size # Creating the unsigned character buffer, and passing it in by reference. buf = (c_ubyte * sz)() - wkb = capi.to_wkb(self.ptr, byteorder, byref(buf)) + capi.to_wkb(self.ptr, byteorder, byref(buf)) # Returning a buffer of the string at the pointer. return memoryview(string_at(buf, sz)) diff --git a/django/contrib/gis/gdal/tests/test_envelope.py b/django/contrib/gis/gdal/tests/test_envelope.py index 14258ff816..4644fbc533 100644 --- a/django/contrib/gis/gdal/tests/test_envelope.py +++ b/django/contrib/gis/gdal/tests/test_envelope.py @@ -22,9 +22,9 @@ class EnvelopeTest(unittest.TestCase): def test01_init(self): "Testing Envelope initilization." e1 = Envelope((0, 0, 5, 5)) - e2 = Envelope(0, 0, 5, 5) - e3 = Envelope(0, '0', '5', 5) # Thanks to ww for this - e4 = Envelope(e1._envelope) + Envelope(0, 0, 5, 5) + Envelope(0, '0', '5', 5) # Thanks to ww for this + Envelope(e1._envelope) self.assertRaises(OGRException, Envelope, (5, 5, 0, 0)) self.assertRaises(OGRException, Envelope, 5, 5, 0, 0) self.assertRaises(OGRException, Envelope, (0, 0, 5, 5, 3)) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index 74b1e894e1..41e7555ab3 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -25,15 +25,12 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): "Testing OGRGeomType object." # OGRGeomType should initialize on all these inputs. - try: - g = OGRGeomType(1) - g = OGRGeomType(7) - g = OGRGeomType('point') - g = OGRGeomType('GeometrycollectioN') - g = OGRGeomType('LINearrING') - g = OGRGeomType('Unknown') - except: - self.fail('Could not create an OGRGeomType object!') + OGRGeomType(1) + OGRGeomType(7) + OGRGeomType('point') + OGRGeomType('GeometrycollectioN') + OGRGeomType('LINearrING') + OGRGeomType('Unknown') # Should throw TypeError on this input self.assertRaises(OGRException, OGRGeomType, 23) @@ -127,7 +124,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): def test02_points(self): "Testing Point objects." - prev = OGRGeometry('POINT(0 0)') + OGRGeometry('POINT(0 0)') for p in self.geometries.points: if not hasattr(p, 'z'): # No 3D pnt = OGRGeometry(p.wkt) @@ -243,7 +240,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): poly = OGRGeometry('POLYGON((0 0, 5 0, 5 5, 0 5), (1 1, 2 1, 2 2, 2 1))') self.assertEqual(8, poly.point_count) with self.assertRaises(OGRException): - _ = poly.centroid + poly.centroid poly.close_rings() self.assertEqual(10, poly.point_count) # Two closing points should've been added @@ -251,7 +248,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin): def test08_multipolygons(self): "Testing MultiPolygon objects." - prev = OGRGeometry('POINT(0 0)') + OGRGeometry('POINT(0 0)') for mp in self.geometries.multipolygons: mpoly = OGRGeometry(mp.wkt) self.assertEqual(6, mpoly.geom_type) diff --git a/django/contrib/gis/gdal/tests/test_srs.py b/django/contrib/gis/gdal/tests/test_srs.py index cacff4be04..33941458c8 100644 --- a/django/contrib/gis/gdal/tests/test_srs.py +++ b/django/contrib/gis/gdal/tests/test_srs.py @@ -58,7 +58,7 @@ class SpatialRefTest(unittest.TestCase): def test01_wkt(self): "Testing initialization on valid OGC WKT." for s in srlist: - srs = SpatialReference(s.wkt) + SpatialReference(s.wkt) def test02_bad_wkt(self): "Testing initialization on invalid WKT." @@ -150,7 +150,7 @@ class SpatialRefTest(unittest.TestCase): target = SpatialReference('WGS84') for s in srlist: if s.proj: - ct = CoordTransform(SpatialReference(s.wkt), target) + CoordTransform(SpatialReference(s.wkt), target) def test13_attr_value(self): "Testing the attr_value() method." diff --git a/django/contrib/gis/geoip/__init__.py b/django/contrib/gis/geoip/__init__.py index 8b519a242d..c42dd2c72b 100644 --- a/django/contrib/gis/geoip/__init__.py +++ b/django/contrib/gis/geoip/__init__.py @@ -11,8 +11,6 @@ Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory corresponding to settings.GEOIP_PATH. """ -from __future__ import absolute_import - try: from .base import GeoIP, GeoIPException HAS_GEOIP = True diff --git a/django/contrib/gis/geoip/base.py b/django/contrib/gis/geoip/base.py index d4793a2ae9..c6e27866db 100644 --- a/django/contrib/gis/geoip/base.py +++ b/django/contrib/gis/geoip/base.py @@ -18,7 +18,8 @@ free_regex = re.compile(r'^GEO-\d{3}FREE') lite_regex = re.compile(r'^GEO-\d{3}LITE') #### GeoIP classes #### -class GeoIPException(Exception): pass +class GeoIPException(Exception): + pass class GeoIP(object): # The flags for GeoIP memory caching. diff --git a/django/contrib/gis/geoip/prototypes.py b/django/contrib/gis/geoip/prototypes.py index 283d721395..c13dd8aba2 100644 --- a/django/contrib/gis/geoip/prototypes.py +++ b/django/contrib/gis/geoip/prototypes.py @@ -14,7 +14,7 @@ class GeoIPRecord(Structure): ('longitude', c_float), # TODO: In 1.4.6 this changed from `int dma_code;` to # `union {int metro_code; int dma_code;};`. Change - # to a `ctypes.Union` in to accomodate in future when + # to a `ctypes.Union` in to accommodate in future when # pre-1.4.6 versions are no longer distributed. ('dma_code', c_int), ('area_code', c_int), diff --git a/django/contrib/gis/geometry/backend/__init__.py b/django/contrib/gis/geometry/backend/__init__.py index d9f30bb256..5830e482f9 100644 --- a/django/contrib/gis/geometry/backend/__init__.py +++ b/django/contrib/gis/geometry/backend/__init__.py @@ -1,11 +1,12 @@ +from importlib import import_module + from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos') try: - module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend') + module = import_module('django.contrib.gis.geometry.backend.%s' % geom_backend) except ImportError: try: module = import_module(geom_backend) diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index b088ec2dc4..7021ed1866 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -18,7 +18,6 @@ from django.contrib.gis.geos.base import GEOSBase, gdal from django.contrib.gis.geos.coordseq import GEOSCoordSeq from django.contrib.gis.geos.error import GEOSException, GEOSIndexError from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE -from django.contrib.gis.geos.mutable_list import ListMixin # All other functions in this module come from the ctypes # prototypes module -- which handles all interaction with diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 900a0adb40..87aba723b9 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -124,24 +124,16 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(hexewkb_3d, pnt_3d.hexewkb) self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz) else: - try: - hexewkb = pnt_3d.hexewkb - except GEOSException: - pass - else: - self.fail('Should have raised GEOSException.') + with self.assertRaises(GEOSException): + pnt_3d.hexewkb # Same for EWKB. self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) if GEOS_PREPARE: self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) else: - try: - ewkb = pnt_3d.ewkb - except GEOSException: - pass - else: - self.fail('Should have raised GEOSException') + with self.assertRaises(GEOSException): + pnt_3d.ewkb # Redundant sanity check. self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) @@ -158,7 +150,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # string-based for err in self.geometries.errors: with self.assertRaises((GEOSException, ValueError)): - _ = fromstr(err.wkt) + fromstr(err.wkt) # Bad WKB self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0')) diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index 8915f01e50..5e74225f91 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from unittest import skipUnless diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index 6c17003982..6fdf67042b 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import os import re diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index df4158bb31..295c1de46f 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from unittest import skipUnless diff --git a/django/contrib/gis/tests/geoapp/feeds.py b/django/contrib/gis/tests/geoapp/feeds.py index f53431c24d..9ec959ecce 100644 --- a/django/contrib/gis/tests/geoapp/feeds.py +++ b/django/contrib/gis/tests/geoapp/feeds.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.contrib.gis import feeds diff --git a/django/contrib/gis/tests/geoapp/sitemaps.py b/django/contrib/gis/tests/geoapp/sitemaps.py index 0e85fda662..55bf7c764c 100644 --- a/django/contrib/gis/tests/geoapp/sitemaps.py +++ b/django/contrib/gis/tests/geoapp/sitemaps.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.gis.sitemaps import GeoRSSSitemap, KMLSitemap, KMZSitemap from .feeds import feed_dict diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py index b2953b4a70..9c7b572e99 100644 --- a/django/contrib/gis/tests/geoapp/test_feeds.py +++ b/django/contrib/gis/tests/geoapp/test_feeds.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from unittest import skipUnless from xml.dom import minidom diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 2ffbaec9a9..844f03aef5 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -1,5 +1,5 @@ # -*- encoding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime from unittest import skipUnless diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index 98cd8cc5ac..bb68039bc3 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from io import BytesIO from unittest import skipUnless diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index eabc5958c0..9221f99285 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import re import unittest diff --git a/django/contrib/gis/tests/geoapp/urls.py b/django/contrib/gis/tests/geoapp/urls.py index 55a5fa3670..70db62d71b 100644 --- a/django/contrib/gis/tests/geoapp/urls.py +++ b/django/contrib/gis/tests/geoapp/urls.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.conf.urls import patterns diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py index 2a60f623fa..bc809bdfc0 100644 --- a/django/contrib/gis/tests/geogapp/tests.py +++ b/django/contrib/gis/tests/geogapp/tests.py @@ -1,7 +1,7 @@ """ Tests for geography support in PostGIS 1.5+ """ -from __future__ import absolute_import +from __future__ import unicode_literals import os from unittest import skipUnless diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py index 34dab1ab7d..4b7c5ff21d 100644 --- a/django/contrib/gis/tests/inspectapp/tests.py +++ b/django/contrib/gis/tests/inspectapp/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import os from unittest import skipUnless diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 3b040624f3..632cb98aeb 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from copy import copy from decimal import Decimal diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 653bda8aaf..83da000f82 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from unittest import skipUnless diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index 5bb6a70206..498d072926 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -24,7 +24,7 @@ test_srs = ({'srid' : 4326, 'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"', 'proj4_re' : r'\+proj=lcc \+lat_1=30.28333333333333 \+lat_2=28.38333333333333 \+lat_0=27.83333333333333 ' r'\+lon_0=-99 \+x_0=600000 \+y_0=4000000 (\+ellps=GRS80 )?' - r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0)?\+units=m \+no_defs ', + r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0 )?\+units=m \+no_defs ', 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central', 'geographic' : False, 'projected' : True, 'spatialite' : False, 'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) diff --git a/django/contrib/messages/__init__.py b/django/contrib/messages/__init__.py index 68a53d996f..a835f29dc9 100644 --- a/django/contrib/messages/__init__.py +++ b/django/contrib/messages/__init__.py @@ -1,4 +1,2 @@ -from __future__ import absolute_import - from django.contrib.messages.api import * from django.contrib.messages.constants import * diff --git a/django/contrib/sessions/management/commands/clearsessions.py b/django/contrib/sessions/management/commands/clearsessions.py index 8eb23dfee0..fa0dad31c3 100644 --- a/django/contrib/sessions/management/commands/clearsessions.py +++ b/django/contrib/sessions/management/commands/clearsessions.py @@ -1,6 +1,7 @@ +from importlib import import_module + from django.conf import settings from django.core.management.base import NoArgsCommand -from django.utils.importlib import import_module class Command(NoArgsCommand): diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py index 8bc2d37dd3..e8ebf3d5f9 100644 --- a/django/contrib/sessions/middleware.py +++ b/django/contrib/sessions/middleware.py @@ -1,9 +1,9 @@ +from importlib import import_module import time from django.conf import settings from django.utils.cache import patch_vary_headers from django.utils.http import cookie_date -from django.utils.importlib import import_module class SessionMiddleware(object): def __init__(self): diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 1acae8296c..627813a33e 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -86,17 +86,27 @@ class Sitemap(object): domain = site.domain urls = [] + latest_lastmod = None + all_items_lastmod = True # track if all items have a lastmod for item in self.paginator.page(page).object_list: loc = "%s://%s%s" % (protocol, domain, self.__get('location', item)) priority = self.__get('priority', item, None) + lastmod = self.__get('lastmod', item, None) + if all_items_lastmod: + all_items_lastmod = lastmod is not None + if (all_items_lastmod and + (latest_lastmod is None or lastmod > latest_lastmod)): + latest_lastmod = lastmod url_info = { 'item': item, 'location': loc, - 'lastmod': self.__get('lastmod', item, None), + 'lastmod': lastmod, 'changefreq': self.__get('changefreq', item, None), 'priority': str(priority if priority is not None else ''), } urls.append(url_info) + if all_items_lastmod: + self.latest_lastmod = latest_lastmod return urls class FlatPageSitemap(Sitemap): diff --git a/django/contrib/sitemaps/tests/test_http.py b/django/contrib/sitemaps/tests/test_http.py index f68f345815..c870b442de 100644 --- a/django/contrib/sitemaps/tests/test_http.py +++ b/django/contrib/sitemaps/tests/test_http.py @@ -77,6 +77,21 @@ class HTTPSitemapTests(SitemapTestsBase): """ % (self.base_url, date.today()) self.assertXMLEqual(response.content.decode('utf-8'), expected_content) + def test_sitemap_last_modified(self): + "Tests that Last-Modified header is set correctly" + response = self.client.get('/lastmod/sitemap.xml') + self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT') + + def test_sitemap_last_modified_missing(self): + "Tests that Last-Modified header is missing when sitemap has no lastmod" + response = self.client.get('/generic/sitemap.xml') + self.assertFalse(response.has_header('Last-Modified')) + + def test_sitemap_last_modified_mixed(self): + "Tests that Last-Modified header is omitted when lastmod not on all items" + response = self.client.get('/lastmod-mixed/sitemap.xml') + self.assertFalse(response.has_header('Last-Modified')) + @skipUnless(settings.USE_I18N, "Internationalization is not enabled") @override_settings(USE_L10N=True) def test_localized_priority(self): diff --git a/django/contrib/sitemaps/tests/urls/http.py b/django/contrib/sitemaps/tests/urls/http.py index a8b804fd4b..6721d72b81 100644 --- a/django/contrib/sitemaps/tests/urls/http.py +++ b/django/contrib/sitemaps/tests/urls/http.py @@ -15,10 +15,36 @@ class SimpleSitemap(Sitemap): def items(self): return [object()] + +class FixedLastmodSitemap(SimpleSitemap): + lastmod = datetime(2013, 3, 13, 10, 0, 0) + + +class FixedLastmodMixedSitemap(Sitemap): + changefreq = "never" + priority = 0.5 + location = '/location/' + loop = 0 + + def items(self): + o1 = TestModel() + o1.lastmod = datetime(2013, 3, 13, 10, 0, 0) + o2 = TestModel() + return [o1, o2] + + simple_sitemaps = { 'simple': SimpleSitemap, } +fixed_lastmod_sitemaps = { + 'fixed-lastmod': FixedLastmodSitemap, +} + +fixed_lastmod__mixed_sitemaps = { + 'fixed-lastmod-mixed': FixedLastmodMixedSitemap, +} + generic_sitemaps = { 'generic': GenericSitemap({'queryset': TestModel.objects.all()}), } @@ -36,6 +62,8 @@ urlpatterns = patterns('django.contrib.sitemaps.views', (r'^simple/sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps}), (r'^simple/custom-sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap.xml'}), + (r'^lastmod/sitemap\.xml$', 'sitemap', {'sitemaps': fixed_lastmod_sitemaps}), + (r'^lastmod-mixed/sitemap\.xml$', 'sitemap', {'sitemaps': fixed_lastmod__mixed_sitemaps}), (r'^generic/sitemap\.xml$', 'sitemap', {'sitemaps': generic_sitemaps}), (r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}), url(r'^cached/index\.xml$', cache_page(1)(views.index), diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py index 7a94fb355d..14983acea9 100644 --- a/django/contrib/sitemaps/views.py +++ b/django/contrib/sitemaps/views.py @@ -1,3 +1,4 @@ +from calendar import timegm from functools import wraps from django.contrib.sites.models import get_current_site @@ -6,6 +7,7 @@ from django.core.paginator import EmptyPage, PageNotAnInteger from django.http import Http404 from django.template.response import TemplateResponse from django.utils import six +from django.utils.http import http_date def x_robots_tag(func): @wraps(func) @@ -64,5 +66,11 @@ def sitemap(request, sitemaps, section=None, raise Http404("Page %s empty" % page) except PageNotAnInteger: raise Http404("No page '%s'" % page) - return TemplateResponse(request, template_name, {'urlset': urls}, - content_type=content_type) + response = TemplateResponse(request, template_name, {'urlset': urls}, + content_type=content_type) + if hasattr(site, 'latest_lastmod'): + # if latest_lastmod is defined for site, set header so as + # ConditionalGetMiddleware is able to send 304 NOT MODIFIED + response['Last-Modified'] = http_date( + timegm(site.latest_lastmod.utctimetuple())) + return response diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index 7d266d95a0..d4efd1a8d8 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -1,8 +1,9 @@ +from collections import OrderedDict import os + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage, Storage, FileSystemStorage -from django.utils.datastructures import SortedDict from django.utils.functional import empty, memoize, LazyObject from django.utils.module_loading import import_by_path from django.utils._os import safe_join @@ -11,7 +12,7 @@ from django.utils import six from django.contrib.staticfiles import utils from django.contrib.staticfiles.storage import AppStaticStorage -_finders = SortedDict() +_finders = OrderedDict() class BaseFinder(object): @@ -47,7 +48,7 @@ class FileSystemFinder(BaseFinder): # List of locations with static files self.locations = [] # Maps dir paths to an appropriate storage instance - self.storages = SortedDict() + self.storages = OrderedDict() if not isinstance(settings.STATICFILES_DIRS, (list, tuple)): raise ImproperlyConfigured( "Your STATICFILES_DIRS setting is not a tuple or list; " @@ -118,7 +119,7 @@ class AppDirectoriesFinder(BaseFinder): # The list of apps that are handled self.apps = [] # Mapping of app module paths to storage instances - self.storages = SortedDict() + self.storages = OrderedDict() if apps is None: apps = settings.INSTALLED_APPS for app in apps: diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index 7c3de80e93..c1e9fa811b 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -2,12 +2,12 @@ from __future__ import unicode_literals import os import sys +from collections import OrderedDict from optparse import make_option from django.core.files.storage import FileSystemStorage from django.core.management.base import CommandError, NoArgsCommand from django.utils.encoding import smart_text -from django.utils.datastructures import SortedDict from django.utils.six.moves import input from django.contrib.staticfiles import finders, storage @@ -97,7 +97,7 @@ class Command(NoArgsCommand): else: handler = self.copy_file - found_files = SortedDict() + found_files = OrderedDict() for finder in finders.get_finders(): for path, storage in finder.list(self.ignore_patterns): # Prefix the relative path if the source storage contains it diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index d085cf723f..1242afe411 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from collections import OrderedDict import hashlib +from importlib import import_module import os import posixpath import re @@ -15,10 +17,8 @@ from django.core.cache import (get_cache, InvalidCacheBackendError, from django.core.exceptions import ImproperlyConfigured from django.core.files.base import ContentFile from django.core.files.storage import FileSystemStorage, get_storage_class -from django.utils.datastructures import SortedDict from django.utils.encoding import force_bytes, force_text from django.utils.functional import LazyObject -from django.utils.importlib import import_module from django.utils._os import upath from django.contrib.staticfiles.utils import check_settings, matches_patterns @@ -64,7 +64,7 @@ class CachedFilesMixin(object): except InvalidCacheBackendError: # Use the default backend self.cache = default_cache - self._patterns = SortedDict() + self._patterns = OrderedDict() for extension, patterns in self.patterns: for pattern in patterns: if isinstance(pattern, (tuple, list)): @@ -202,7 +202,7 @@ class CachedFilesMixin(object): def post_process(self, paths, dry_run=False, **options): """ - Post process the given list of files (called from collectstatic). + Post process the given OrderedDict of files (called from collectstatic). Processing is actually two separate operations: diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py index f5c42fedf8..7ddc6a1bc2 100644 --- a/django/contrib/staticfiles/views.py +++ b/django/contrib/staticfiles/views.py @@ -11,7 +11,6 @@ except ImportError: # Python 2 from urllib import unquote from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.http import Http404 from django.views import static @@ -31,9 +30,7 @@ def serve(request, path, insecure=False, **kwargs): It uses the django.views.static view to serve the found files. """ if not settings.DEBUG and not insecure: - raise ImproperlyConfigured("The staticfiles view can only be used in " - "debug mode or if the --insecure " - "option of 'runserver' is used") + raise Http404 normalized_path = posixpath.normpath(unquote(path)).lstrip('/') absolute_path = finders.find(normalized_path) if not absolute_path: diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py index 1242372dcf..e1d2eb455a 100644 --- a/django/core/cache/__init__.py +++ b/django/core/cache/__init__.py @@ -14,6 +14,7 @@ cache class. See docs/topics/cache.txt for information on the public API. """ +import importlib try: from urllib.parse import parse_qsl except ImportError: # Python 2 @@ -24,7 +25,6 @@ from django.core import signals from django.core.cache.backends.base import ( InvalidCacheBackendError, CacheKeyWarning, BaseCache) from django.core.exceptions import ImproperlyConfigured -from django.utils import importlib from django.utils.module_loading import import_by_path diff --git a/django/core/cache/utils.py b/django/core/cache/utils.py index 4310825ad4..b9806cc5e0 100644 --- a/django/core/cache/utils.py +++ b/django/core/cache/utils.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import hashlib from django.utils.encoding import force_bytes diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 38d8154ac9..967254dfac 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -11,7 +11,7 @@ from django.core import signals from django.core.handlers import base from django.core.urlresolvers import set_script_prefix from django.utils import datastructures -from django.utils.encoding import force_str, force_text, iri_to_uri +from django.utils.encoding import force_str # For backwards compatibility -- lots of code uses this in the wild! from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py index fcff803376..1e2b35cc2f 100644 --- a/django/core/mail/__init__.py +++ b/django/core/mail/__init__.py @@ -32,7 +32,7 @@ def get_connection(backend=None, fail_silently=False, **kwds): def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, - connection=None): + connection=None, html_message=None): """ Easy wrapper for sending a single message to a recipient list. All members of the recipient list will see the other recipients in the 'To' field. @@ -46,8 +46,12 @@ def send_mail(subject, message, from_email, recipient_list, connection = connection or get_connection(username=auth_user, password=auth_password, fail_silently=fail_silently) - return EmailMessage(subject, message, from_email, recipient_list, - connection=connection).send() + mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, + connection=connection) + if html_message: + mail.attach_alternative(html_message, 'text/html') + + return mail.send() def send_mass_mail(datatuple, fail_silently=False, auth_user=None, diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 8fd46aa759..2ac797ecfe 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -1,13 +1,13 @@ import collections +import imp +from importlib import import_module +from optparse import OptionParser, NO_DEFAULT import os import sys -from optparse import OptionParser, NO_DEFAULT -import imp from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError, handle_default_options from django.core.management.color import color_style -from django.utils.importlib import import_module from django.utils import six # For backwards compatibility: get_version() used to be in this module. @@ -146,7 +146,7 @@ def call_command(name, *args, **options): # Grab out a list of defaults from the options. optparse does this for us # when the script runs from the command line, but since call_command can - # be called programatically, we need to simulate the loading and handling + # be called programmatically, we need to simulate the loading and handling # of defaults (see #10080 for details). defaults = {} for opt in klass.option_list: diff --git a/django/core/management/base.py b/django/core/management/base.py index af040288d0..9c8940e99f 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -10,7 +10,7 @@ from optparse import make_option, OptionParser import django from django.core.exceptions import ImproperlyConfigured -from django.core.management.color import color_style +from django.core.management.color import color_style, no_style from django.utils.encoding import force_str from django.utils.six import StringIO @@ -171,6 +171,8 @@ class BaseCommand(object): help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'), make_option('--traceback', action='store_true', help='Raise on exception'), + make_option('--no-color', action='store_true', dest='no_color', default=False, + help="Don't colorize the command output."), ) help = '' args = '' @@ -254,7 +256,11 @@ class BaseCommand(object): ``self.requires_model_validation``, except if force-skipped). """ self.stdout = OutputWrapper(options.get('stdout', sys.stdout)) - self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) + if options.get('no_color'): + self.style = no_style() + self.stderr = OutputWrapper(options.get('stderr', sys.stderr)) + else: + self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) if self.can_import_settings: from django.conf import settings diff --git a/django/core/management/color.py b/django/core/management/color.py index 8c7a87fe71..708823705b 100644 --- a/django/core/management/color.py +++ b/django/core/management/color.py @@ -30,7 +30,7 @@ def color_style(): class dummy: pass style = dummy() # The nocolor palette has all available roles. - # Use that pallete as the basis for populating + # Use that palette as the basis for populating # the palette as defined in the environment. for role in termcolors.PALETTES[termcolors.NOCOLOR_PALETTE]: format = color_settings.get(role,{}) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 5e440196fc..c74eede846 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -1,10 +1,11 @@ +from collections import OrderedDict +from optparse import make_option + from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError from django.core import serializers from django.db import router, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict -from optparse import make_option class Command(BaseCommand): option_list = BaseCommand.option_list + ( @@ -22,7 +23,7 @@ class Command(BaseCommand): make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False, help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."), make_option('--pks', dest='primary_keys', help="Only dump objects with " - "given primary keys. Accepts a comma seperated list of keys. " + "given primary keys. Accepts a comma separated list of keys. " "This option will only work when you specify one model."), ) help = ("Output the contents of the database as a fixture of the given " @@ -66,11 +67,11 @@ class Command(BaseCommand): if len(app_labels) == 0: if primary_keys: raise CommandError("You can only use --pks option with one model") - app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps) + app_list = OrderedDict((app, None) for app in get_apps() if app not in excluded_apps) else: if len(app_labels) > 1 and primary_keys: raise CommandError("You can only use --pks option with one model") - app_list = SortedDict() + app_list = OrderedDict() for label in app_labels: try: app_label, model_label = label.split('.') diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index a6ea45ce95..5e951f97b4 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -1,4 +1,5 @@ import sys +from importlib import import_module from optparse import make_option from django.conf import settings diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index dc26bb11d2..2cfea028ec 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals +from collections import OrderedDict import keyword import re from optparse import make_option from django.core.management.base import NoArgsCommand, CommandError from django.db import connections, DEFAULT_DB_ALIAS -from django.utils.datastructures import SortedDict class Command(NoArgsCommand): @@ -69,7 +69,7 @@ class Command(NoArgsCommand): used_column_names = [] # Holds column names used in the table so far for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)): comment_notes = [] # Holds Field notes, to be displayed in a Python comment. - extra_params = SortedDict() # Holds Field parameters such as 'db_column'. + extra_params = OrderedDict() # Holds Field parameters such as 'db_column'. column_name = row[0] is_relation = i in relations @@ -193,7 +193,7 @@ class Command(NoArgsCommand): description, this routine will return the given field type name, as well as any additional keyword parameters and notes for the field. """ - field_params = SortedDict() + field_params = OrderedDict() field_notes = [] try: diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 802226f9d1..0da36a3c52 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import glob import gzip import os +import warnings import zipfile from optparse import make_option import warnings @@ -156,12 +157,13 @@ class Command(BaseCommand): finally: fixture.close() - # If the fixture we loaded contains 0 objects, assume that an - # error was encountered during fixture loading. + # Warn if the fixture we loaded contains 0 objects. if objects_in_fixture == 0: - raise CommandError( - "No fixture data found for '%s'. " - "(File format may be invalid.)" % fixture_name) + warnings.warn( + "No fixture data found for '%s'. (File format may be " + "invalid.)" % fixture_name, + RuntimeWarning + ) def _find_fixtures(self, fixture_label): """ @@ -233,7 +235,7 @@ class Command(BaseCommand): """ dirs = [] for path in get_app_paths(): - d = os.path.join(os.path.dirname(path), 'fixtures') + d = os.path.join(path, 'fixtures') if os.path.isdir(d): dirs.append(d) dirs.extend(list(settings.FIXTURE_DIRS)) diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py index 17b5a7dfe9..699b22edaa 100644 --- a/django/core/management/commands/migrate.py +++ b/django/core/management/commands/migrate.py @@ -1,4 +1,5 @@ from optparse import make_option +from collections import OrderedDict import itertools import traceback @@ -10,7 +11,6 @@ from django.core.management.sql import custom_sql_for_model, emit_post_migrate_s from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import AmbiguityError -from django.utils.datastructures import SortedDict from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule @@ -161,7 +161,7 @@ class Command(BaseCommand): return not ((converter(opts.db_table) in tables) or (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables)) - manifest = SortedDict( + manifest = OrderedDict( (app_name, list(filter(model_installed, model_list))) for app_name, model_list in all_models ) diff --git a/django/core/management/commands/runfcgi.py b/django/core/management/commands/runfcgi.py index a60d4ebc59..4e9331fc80 100644 --- a/django/core/management/commands/runfcgi.py +++ b/django/core/management/commands/runfcgi.py @@ -1,3 +1,5 @@ +import warnings + from django.core.management.base import BaseCommand class Command(BaseCommand): @@ -5,6 +7,10 @@ class Command(BaseCommand): args = '[various KEY=val options, use `runfcgi help` for help]' def handle(self, *args, **options): + warnings.warn( + "FastCGI support has been deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning) + from django.conf import settings from django.utils import translation # Activate the current language, because it won't get activated later. @@ -14,7 +20,7 @@ class Command(BaseCommand): pass from django.core.servers.fastcgi import runfastcgi runfastcgi(args) - + def usage(self, subcommand): from django.core.servers.fastcgi import FASTCGI_HELP return FASTCGI_HELP diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 851d4e3cfb..00a6602c0b 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -19,23 +19,35 @@ class Command(NoArgsCommand): help = "Runs a Python interactive interpreter. Tries to use IPython or bpython, if one of them is available." requires_model_validation = False + def _ipython_pre_011(self): + """Start IPython pre-0.11""" + from IPython.Shell import IPShell + shell = IPShell(argv=[]) + shell.mainloop() + + def _ipython_pre_100(self): + """Start IPython pre-1.0.0""" + from IPython.frontend.terminal.ipapp import TerminalIPythonApp + app = TerminalIPythonApp.instance() + app.initialize(argv=[]) + app.start() + + def _ipython(self): + """Start IPython >= 1.0""" + from IPython import start_ipython + start_ipython(argv=[]) + def ipython(self): - try: - from IPython.frontend.terminal.ipapp import TerminalIPythonApp - app = TerminalIPythonApp.instance() - app.initialize(argv=[]) - app.start() - except ImportError: - # IPython < 0.11 - # Explicitly pass an empty list as arguments, because otherwise - # IPython would use sys.argv from this script. + """Start any version of IPython""" + for ip in (self._ipython, self._ipython_pre_100, self._ipython_pre_011): try: - from IPython.Shell import IPShell - shell = IPShell(argv=[]) - shell.mainloop() + ip() except ImportError: - # IPython not found at all, raise ImportError - raise + pass + else: + return + # no IPython, raise ImportError + raise ImportError("No IPython") def bpython(self): import bpython diff --git a/django/core/management/commands/startapp.py b/django/core/management/commands/startapp.py index 692ad09a43..77e884ca5a 100644 --- a/django/core/management/commands/startapp.py +++ b/django/core/management/commands/startapp.py @@ -1,6 +1,7 @@ +from importlib import import_module + from django.core.management.base import CommandError from django.core.management.templates import TemplateCommand -from django.utils.importlib import import_module class Command(TemplateCommand): diff --git a/django/core/management/commands/startproject.py b/django/core/management/commands/startproject.py index b143e6c380..b7dcbcaba5 100644 --- a/django/core/management/commands/startproject.py +++ b/django/core/management/commands/startproject.py @@ -1,7 +1,8 @@ +from importlib import import_module + from django.core.management.base import CommandError from django.core.management.templates import TemplateCommand from django.utils.crypto import get_random_string -from django.utils.importlib import import_module class Command(TemplateCommand): diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 4a61fcddb9..2e977c0c07 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import codecs import os import re +import warnings from django.conf import settings from django.core.management.base import CommandError @@ -168,7 +169,18 @@ def _split_statements(content): def custom_sql_for_model(model, style, connection): opts = model._meta - app_dir = os.path.normpath(os.path.join(os.path.dirname(upath(models.get_app(model._meta.app_label).__file__)), 'sql')) + app_dirs = [] + app_dir = models.get_app_path(model._meta.app_label) + app_dirs.append(os.path.normpath(os.path.join(app_dir, 'sql'))) + + # Deprecated location -- remove in Django 1.9 + old_app_dir = os.path.normpath(os.path.join(app_dir, 'models/sql')) + if os.path.exists(old_app_dir): + warnings.warn("Custom SQL location '<app_label>/models/sql' is " + "deprecated, use '<app_label>/sql' instead.", + PendingDeprecationWarning) + app_dirs.append(old_app_dir) + output = [] # Post-creation SQL should come before any initial SQL data is loaded. @@ -181,8 +193,10 @@ def custom_sql_for_model(model, style, connection): # Find custom SQL, if it's available. backend_name = connection.settings_dict['ENGINE'].split('.')[-1] - sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)), - os.path.join(app_dir, "%s.sql" % opts.model_name)] + sql_files = [] + for app_dir in app_dirs: + sql_files.append(os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name))) + sql_files.append(os.path.join(app_dir, "%s.sql" % opts.model_name)) for sql_file in sql_files: if os.path.exists(sql_file): with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: diff --git a/django/core/management/utils.py b/django/core/management/utils.py index 7159d1123f..d1052d5182 100644 --- a/django/core/management/utils.py +++ b/django/core/management/utils.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import os from subprocess import PIPE, Popen diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py index c48050415d..dc3d139d3b 100644 --- a/django/core/serializers/__init__.py +++ b/django/core/serializers/__init__.py @@ -16,8 +16,9 @@ To add your own serializers, use the SERIALIZATION_MODULES setting:: """ +import importlib + from django.conf import settings -from django.utils import importlib from django.utils import six from django.core.serializers.base import SerializerDoesNotExist diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index 64357bf9d5..b07aa3392c 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -4,6 +4,7 @@ Serialize data to/from JSON # Avoid shadowing the standard library json module from __future__ import absolute_import +from __future__ import unicode_literals import datetime import decimal diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index 2ae1fa5ae3..a612142969 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -12,9 +12,9 @@ Run with the extra option "help" for a list of additional options you can pass to this server. """ +import importlib import os import sys -from django.utils import importlib __version__ = "0.1" __all__ = ["runfastcgi"] diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index b7017e47b9..9e086a453a 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -8,6 +8,7 @@ a string) and returns a tuple in this format: """ from __future__ import unicode_literals +from importlib import import_module import re from threading import local @@ -17,7 +18,6 @@ from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_str, force_text, iri_to_uri from django.utils.functional import memoize, lazy from django.utils.http import urlquote -from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule from django.utils.regex_helper import normalize from django.utils import six diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 7185644cc3..ec6081678b 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -9,6 +9,7 @@ except ImportError: from django.utils.six.moves import _dummy_thread as thread from collections import namedtuple from contextlib import contextmanager +from importlib import import_module from django.conf import settings from django.db import DEFAULT_DB_ALIAS @@ -17,7 +18,6 @@ from django.db.backends import util from django.db.transaction import TransactionManagementError from django.db.utils import DatabaseErrorWrapper from django.utils.functional import cached_property -from django.utils.importlib import import_module from django.utils import six from django.utils import timezone diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 51716a88bd..1d90f03425 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -148,7 +148,7 @@ class BaseDatabaseCreation(object): Returns any ALTER TABLE statements to add constraints after the fact. """ opts = model._meta - if not opts.managed or opts.proxy or opts.swapped: + if not opts.managed or opts.swapped: return [] qn = self.connection.ops.quote_name final_output = [] diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 63d38bdafd..46022a97b1 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -579,7 +579,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s" % self._standard_operators['contains'], ['X']) - except utils.DatabaseError: + except DatabaseError: self.operators = self._likec_operators else: self.operators = self._standard_operators diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py index d2d4cd3ac9..b6eb80e7ce 100644 --- a/django/db/backends/oracle/compiler.py +++ b/django/db/backends/oracle/compiler.py @@ -25,7 +25,7 @@ class SQLCompiler(compiler.SQLCompiler): def as_sql(self, with_limits=True, with_col_aliases=False): """ Creates the SQL for this query. Returns the SQL string and list - of parameters. This is overriden from the original Query class + of parameters. This is overridden from the original Query class to handle the additional SQL Oracle requires to emulate LIMIT and OFFSET. diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 960b6def03..841ca92f3e 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -338,7 +338,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): if 'check_same_thread' in kwargs and kwargs['check_same_thread']: warnings.warn( 'The `check_same_thread` option was provided and set to ' - 'True. It will be overriden with False. Use the ' + 'True. It will be overridden with False. Use the ' '`DatabaseWrapper.allow_thread_sharing` property instead ' 'for controlling thread shareability.', RuntimeWarning diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 4d310e480b..2ee525faf1 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,8 +1,8 @@ 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, UnavailableApp -from django.db.models.query import Q +from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp +from django.db.models.query import Q, QuerySet from django.db.models.expressions import F from django.db.models.manager import Manager from django.db.models.base import Model diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index e0bfb9d879..f4c64f7b7d 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -1,8 +1,8 @@ +from collections import OrderedDict from operator import attrgetter from django.db import connections, transaction, IntegrityError from django.db.models import signals, sql -from django.utils.datastructures import SortedDict from django.utils import six @@ -234,7 +234,7 @@ class Collector(object): found = True if not found: return - self.data = SortedDict([(model, self.data[model]) + self.data = OrderedDict([(model, self.data[model]) for model in sorted_models]) def delete(self): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 0410815c92..4135c60ad3 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -17,7 +17,7 @@ from django import forms from django.core import exceptions, validators from django.utils.datastructures import DictWrapper from django.utils.dateparse import parse_date, parse_datetime, parse_time -from django.utils.functional import curry, total_ordering +from django.utils.functional import curry, total_ordering, Promise from django.utils.text import capfirst from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -90,7 +90,7 @@ class Field(object): 'already exists.'), } - # Generic field type description, usually overriden by subclasses + # Generic field type description, usually overridden by subclasses def _description(self): return _('Field of type: %(field_type)s') % { 'field_type': self.__class__.__name__ @@ -441,6 +441,8 @@ class Field(object): """ Perform preliminary non-db specific value checks and conversions. """ + if isinstance(value, Promise): + value = value._proxy____cast() return value def get_db_prep_value(self, value, connection, prepared=False): @@ -562,7 +564,14 @@ class Field(object): def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): """Returns choices with a default blank choices included, for use as SelectField choices for this field.""" - first_choice = blank_choice if include_blank else [] + blank_defined = False + for choice, _ in self.choices: + if choice in ('', None): + blank_defined = True + break + + first_choice = (blank_choice if include_blank and + not blank_defined else []) if self.choices: return first_choice + list(self.choices) rel_model = self.rel.to @@ -724,6 +733,7 @@ class AutoField(Field): return value def get_prep_value(self, value): + value = super(AutoField, self).get_prep_value(value) if value is None: return None return int(value) @@ -783,6 +793,7 @@ class BooleanField(Field): return super(BooleanField, self).get_prep_lookup(lookup_type, value) def get_prep_value(self, value): + value = super(BooleanField, self).get_prep_value(value) if value is None: return None return bool(value) @@ -816,6 +827,7 @@ class CharField(Field): return smart_text(value) def get_prep_value(self, value): + value = super(CharField, self).get_prep_value(value) return self.to_python(value) def formfield(self, **kwargs): @@ -931,6 +943,7 @@ class DateField(Field): return super(DateField, self).get_prep_lookup(lookup_type, value) def get_prep_value(self, value): + value = super(DateField, self).get_prep_value(value) return self.to_python(value) def get_db_prep_value(self, value, connection, prepared=False): @@ -1028,6 +1041,7 @@ class DateTimeField(DateField): # get_prep_lookup is inherited from DateField def get_prep_value(self, value): + value = super(DateTimeField, self).get_prep_value(value) value = self.to_python(value) if value is not None and settings.USE_TZ and timezone.is_naive(value): # For backwards compatibility, interpret naive datetimes in local @@ -1116,6 +1130,7 @@ class DecimalField(Field): self.max_digits, self.decimal_places) def get_prep_value(self, value): + value = super(DecimalField, self).get_prep_value(value) return self.to_python(value) def formfield(self, **kwargs): @@ -1205,6 +1220,7 @@ class FloatField(Field): description = _("Floating point number") def get_prep_value(self, value): + value = super(FloatField, self).get_prep_value(value) if value is None: return None return float(value) @@ -1238,6 +1254,7 @@ class IntegerField(Field): description = _("Integer") def get_prep_value(self, value): + value = super(IntegerField, self).get_prep_value(value) if value is None: return None return int(value) @@ -1346,6 +1363,7 @@ class GenericIPAddressField(Field): return value or None def get_prep_value(self, value): + value = super(GenericIPAddressField, self).get_prep_value(value) if value and ':' in value: try: return clean_ipv6_address(value, self.unpack_ipv4) @@ -1411,6 +1429,7 @@ class NullBooleanField(Field): value) def get_prep_value(self, value): + value = super(NullBooleanField, self).get_prep_value(value) if value is None: return None return bool(value) @@ -1493,6 +1512,7 @@ class TextField(Field): return "TextField" def get_prep_value(self, value): + value = super(TextField, self).get_prep_value(value) if isinstance(value, six.string_types) or value is None: return value return smart_text(value) @@ -1569,6 +1589,7 @@ class TimeField(Field): return super(TimeField, self).pre_save(model_instance, add) def get_prep_value(self, value): + value = super(TimeField, self).get_prep_value(value) return self.to_python(value) def get_db_prep_value(self, value, connection, prepared=False): diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 311f74a905..61e3eebf49 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -253,6 +253,7 @@ class FileField(Field): def get_prep_value(self, value): "Returns field's value prepared for saving into a database." + value = super(FileField, self).get_prep_value(value) # Need to convert File objects provided via a form to unicode for database insertion if value is None: return None diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5a7718e880..00da186279 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -294,10 +294,15 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec params = dict( (rh_field.attname, getattr(instance, lh_field.attname)) for lh_field, rh_field in self.field.related_fields) - params.update(self.field.get_extra_descriptor_filter(instance)) qs = self.get_queryset(instance=instance) + extra_filter = self.field.get_extra_descriptor_filter(instance) + if isinstance(extra_filter, dict): + params.update(extra_filter) + qs = qs.filter(**params) + else: + qs = qs.filter(extra_filter, **params) # Assuming the database enforces foreign keys, this won't fail. - rel_obj = qs.get(**params) + rel_obj = qs.get() if not self.field.rel.multiple: setattr(rel_obj, self.field.related.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) @@ -1003,10 +1008,11 @@ class ForeignObject(RelatedField): user does 'instance.fieldname', that is the extra filter is used in the descriptor of the field. - The filter should be something usable in .filter(**kwargs) call, and - will be ANDed together with the joining columns condition. + The filter should be either a dict usable in .filter(**kwargs) call or + a Q-object. The condition will be ANDed together with the relation's + joining columns. - A parallel method is get_extra_relation_restriction() which is used in + A parallel method is get_extra_restriction() which is used in JOIN and subquery conditions. """ return {} @@ -1054,7 +1060,7 @@ class ForeignObject(RelatedField): value_list = [] for source in sources: # Account for one-to-one relations when sent a different model - while not isinstance(value, source.model): + while not isinstance(value, source.model) and source.rel: source = source.rel.to._meta.get_field(source.rel.field_name) value_list.append(getattr(value, source.attname)) return tuple(value_list) @@ -1119,7 +1125,7 @@ class ForeignObject(RelatedField): class ForeignKey(ForeignObject): empty_strings_allowed = False default_error_messages = { - 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') + 'invalid': _('%(model)s instance with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") diff --git a/django/db/models/loading.py b/django/db/models/loading.py index df3be5fe9b..2858b8b699 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -1,22 +1,31 @@ "Utilities for loading models and the modules that contain them." +from collections import OrderedDict +import copy +import imp +from importlib import import_module +import os +import sys + from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.datastructures import SortedDict -from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule from django.utils._os import upath from django.utils import six -import imp -import sys -import os - __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') MODELS_MODULE_NAME = 'models' +class ModelDict(OrderedDict): + """ + We need to special-case the deepcopy for this, as the keys are modules, + which can't be deep copied. + """ + def __deepcopy__(self, memo): + return self.__class__([(key, copy.deepcopy(value, memo)) + for key, value in self.items()]) class UnavailableApp(Exception): pass @@ -29,14 +38,14 @@ def _initialize(): """ return dict( # Keys of app_store are the model modules for each application. - app_store = SortedDict(), + app_store=ModelDict(), # Mapping of installed app_labels to model modules for that app. app_labels = {}, # Mapping of app_labels to a dictionary of model names to model code. # May contain apps that are not installed. - app_models = SortedDict(), + app_models=ModelDict(), # Mapping of app_labels to errors raised when trying to import the app. app_errors = {}, @@ -118,11 +127,11 @@ class BaseAppCache(object): Loads the app with the provided fully qualified name, and returns the model module. """ + app_module = import_module(app_name) self.handled.add(app_name) self.nesting_level += 1 - app_module = import_module(app_name) try: - models = import_module('.' + MODELS_MODULE_NAME, app_name) + models = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME)) except ImportError: self.nesting_level -= 1 # If the app doesn't have a models module, we can just ignore the @@ -176,6 +185,16 @@ class BaseAppCache(object): return [elt[0] for elt in apps] + def _get_app_path(self, app): + if hasattr(app, '__path__'): # models/__init__.py package + app_path = app.__path__[0] + else: # models.py module + app_path = app.__file__ + return os.path.dirname(upath(app_path)) + + def get_app_path(self, app_label): + return self._get_app_path(self.get_app(app_label)) + def get_app_paths(self): """ Returns a list of paths to all installed apps. @@ -187,10 +206,7 @@ class BaseAppCache(object): app_paths = [] for app in self.get_apps(): - if hasattr(app, '__path__'): # models/__init__.py package - app_paths.extend([upath(path) for path in app.__path__]) - else: # models.py module - app_paths.append(upath(app.__file__)) + app_paths.append(self._get_app_path(app)) return app_paths def get_app(self, app_label, emptyOK=False): @@ -262,12 +278,12 @@ class BaseAppCache(object): if app_mod: if app_mod in self.app_store: app_list = [self.app_models.get(self._label_for(app_mod), - SortedDict())] + ModelDict())] else: app_list = [] else: if only_installed: - app_list = [self.app_models.get(app_label, SortedDict()) + app_list = [self.app_models.get(app_label, ModelDict()) for app_label in six.iterkeys(self.app_labels)] else: app_list = six.itervalues(self.app_models) @@ -318,7 +334,7 @@ class BaseAppCache(object): # Store as 'name: model' pair in a dictionary # in the app_models dictionary model_name = model._meta.model_name - model_dict = self.app_models.setdefault(app_label, SortedDict()) + model_dict = self.app_models.setdefault(app_label, ModelDict()) if model_name in model_dict: # The same model may be imported via different paths (e.g. # appname.models and project.appname.models). We use the source @@ -364,6 +380,7 @@ cache = AppCache() # These methods were always module level, so are kept that way for backwards # compatibility. get_apps = cache.get_apps +get_app_path = cache.get_app_path get_app_paths = cache.get_app_paths get_app = cache.get_app get_app_errors = cache.get_app_errors diff --git a/django/db/models/manager.py b/django/db/models/manager.py index b369aedb64..4f16b5ebfe 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,6 +1,8 @@ import copy +import inspect + from django.db import router -from django.db.models.query import QuerySet, insert_query, RawQuerySet +from django.db.models.query import QuerySet from django.db.models import signals from django.db.models.fields import FieldDoesNotExist from django.utils import six @@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase): ) -class Manager(six.with_metaclass(RenameManagerMethods)): +class BaseManager(six.with_metaclass(RenameManagerMethods)): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 def __init__(self): - super(Manager, self).__init__() + super(BaseManager, self).__init__() self._set_creation_counter() self.model = None self._inherited = False self._db = None + @classmethod + def _get_queryset_methods(cls, queryset_class): + def create_method(name, method): + def manager_method(self, *args, **kwargs): + return getattr(self.get_queryset(), name)(*args, **kwargs) + manager_method.__name__ = method.__name__ + manager_method.__doc__ = method.__doc__ + return manager_method + + new_methods = {} + # Refs http://bugs.python.org/issue1785. + predicate = inspect.isfunction if six.PY3 else inspect.ismethod + for name, method in inspect.getmembers(queryset_class, predicate=predicate): + # Only copy missing methods. + if hasattr(cls, name): + continue + # Only copy public methods or methods with the attribute `queryset_only=False`. + queryset_only = getattr(method, 'queryset_only', None) + if queryset_only or (queryset_only is None and name.startswith('_')): + continue + # Copy the method onto the manager. + new_methods[name] = create_method(name, method) + return new_methods + + @classmethod + def from_queryset(cls, queryset_class, class_name=None): + if class_name is None: + class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__) + class_dict = { + '_queryset_class': queryset_class, + } + class_dict.update(cls._get_queryset_methods(queryset_class)) + return type(class_name, (cls,), class_dict) + def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model @@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)): Sets the creation counter value for this instance and increments the class-level copy. """ - self.creation_counter = Manager.creation_counter - Manager.creation_counter += 1 + self.creation_counter = BaseManager.creation_counter + BaseManager.creation_counter += 1 def _copy_to_model(self, model): """ @@ -117,129 +153,23 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def db(self): return self._db or router.db_for_read(self.model) - ####################### - # PROXIES TO QUERYSET # - ####################### - def get_queryset(self): - """Returns a new QuerySet object. Subclasses can override this method - to easily customize the behavior of the Manager. """ - return QuerySet(self.model, using=self._db) - - def none(self): - return self.get_queryset().none() + Returns a new QuerySet object. Subclasses can override this method to + easily customize the behavior of the Manager. + """ + return self._queryset_class(self.model, using=self._db) def all(self): + # We can't proxy this method through the `QuerySet` like we do for the + # rest of the `QuerySet` methods. This is because `QuerySet.all()` + # works by creating a "copy" of the current queryset and in making said + # copy, all the cached `prefetch_related` lookups are lost. See the + # implementation of `RelatedManager.get_queryset()` for a better + # understanding of how this comes into play. return self.get_queryset() - def count(self): - return self.get_queryset().count() - - def dates(self, *args, **kwargs): - return self.get_queryset().dates(*args, **kwargs) - - def datetimes(self, *args, **kwargs): - return self.get_queryset().datetimes(*args, **kwargs) - - def distinct(self, *args, **kwargs): - return self.get_queryset().distinct(*args, **kwargs) - - def extra(self, *args, **kwargs): - return self.get_queryset().extra(*args, **kwargs) - - def get(self, *args, **kwargs): - return self.get_queryset().get(*args, **kwargs) - - def get_or_create(self, **kwargs): - return self.get_queryset().get_or_create(**kwargs) - - def update_or_create(self, **kwargs): - return self.get_queryset().update_or_create(**kwargs) - - def create(self, **kwargs): - return self.get_queryset().create(**kwargs) - - def bulk_create(self, *args, **kwargs): - return self.get_queryset().bulk_create(*args, **kwargs) - - def filter(self, *args, **kwargs): - return self.get_queryset().filter(*args, **kwargs) - - def aggregate(self, *args, **kwargs): - return self.get_queryset().aggregate(*args, **kwargs) - - def annotate(self, *args, **kwargs): - return self.get_queryset().annotate(*args, **kwargs) - - def complex_filter(self, *args, **kwargs): - return self.get_queryset().complex_filter(*args, **kwargs) - - def exclude(self, *args, **kwargs): - return self.get_queryset().exclude(*args, **kwargs) - - def in_bulk(self, *args, **kwargs): - return self.get_queryset().in_bulk(*args, **kwargs) - - def iterator(self, *args, **kwargs): - return self.get_queryset().iterator(*args, **kwargs) - - def earliest(self, *args, **kwargs): - return self.get_queryset().earliest(*args, **kwargs) - - def latest(self, *args, **kwargs): - return self.get_queryset().latest(*args, **kwargs) - - def first(self): - return self.get_queryset().first() - - def last(self): - return self.get_queryset().last() - - def order_by(self, *args, **kwargs): - return self.get_queryset().order_by(*args, **kwargs) - - def select_for_update(self, *args, **kwargs): - return self.get_queryset().select_for_update(*args, **kwargs) - - def select_related(self, *args, **kwargs): - return self.get_queryset().select_related(*args, **kwargs) - - def prefetch_related(self, *args, **kwargs): - return self.get_queryset().prefetch_related(*args, **kwargs) - - def values(self, *args, **kwargs): - return self.get_queryset().values(*args, **kwargs) - - def values_list(self, *args, **kwargs): - return self.get_queryset().values_list(*args, **kwargs) - - def update(self, *args, **kwargs): - return self.get_queryset().update(*args, **kwargs) - - def reverse(self, *args, **kwargs): - return self.get_queryset().reverse(*args, **kwargs) - - def defer(self, *args, **kwargs): - return self.get_queryset().defer(*args, **kwargs) - - def only(self, *args, **kwargs): - return self.get_queryset().only(*args, **kwargs) - - def using(self, *args, **kwargs): - return self.get_queryset().using(*args, **kwargs) - - def exists(self, *args, **kwargs): - return self.get_queryset().exists(*args, **kwargs) - - def _insert(self, objs, fields, **kwargs): - return insert_query(self.model, objs, fields, **kwargs) - - def _update(self, values, **kwargs): - return self.get_queryset()._update(values, **kwargs) - - def raw(self, raw_query, params=None, *args, **kwargs): - return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) +Manager = BaseManager.from_queryset(QuerySet, class_name='Manager') class ManagerDescriptor(object): diff --git a/django/db/models/options.py b/django/db/models/options.py index 1cacbc776c..e6901fb297 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from collections import OrderedDict import re from bisect import bisect import warnings @@ -11,7 +12,6 @@ from django.db.models.fields.proxy import OrderWrt from django.db.models.loading import get_models, app_cache_ready, cache from django.utils import six from django.utils.functional import cached_property -from django.utils.datastructures import SortedDict from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible from django.utils.translation import activate, deactivate_all, get_language, string_concat @@ -40,7 +40,6 @@ class Options(object): self.get_latest_by = None self.order_with_respect_to = None self.db_tablespace = settings.DEFAULT_TABLESPACE - self.admin = None self.meta = meta self.pk = None self.has_auto_field, self.auto_field = False, None @@ -58,7 +57,7 @@ class Options(object): # concrete models, the concrete_model is always the class itself. self.concrete_model = None self.swappable = None - self.parents = SortedDict() + self.parents = OrderedDict() self.auto_created = False # To handle various inheritance situations, we need to track where @@ -212,9 +211,10 @@ class Options(object): def pk_index(self): """ - Returns the index of the primary key field in the self.fields list. + Returns the index of the primary key field in the self.concrete_fields + list. """ - return self.fields.index(self.pk) + return self.concrete_fields.index(self.pk) def setup_proxy(self, target): """ @@ -340,7 +340,7 @@ class Options(object): return list(six.iteritems(self._m2m_cache)) def _fill_m2m_cache(self): - cache = SortedDict() + cache = OrderedDict() for parent in self.parents: for field, model in parent._meta.get_m2m_with_model(): if model: @@ -411,12 +411,13 @@ class Options(object): for f, model in self.get_all_related_objects_with_model(): cache[f.field.related_query_name()] = (f, model, False, False) for f, model in self.get_m2m_with_model(): - cache[f.name] = (f, model, True, True) + cache[f.name] = cache[f.attname] = (f, model, True, True) for f, model in self.get_fields_with_model(): - cache[f.name] = (f, model, True, False) + cache[f.name] = cache[f.attname] = (f, model, True, False) for f in self.virtual_fields: if hasattr(f, 'related'): - cache[f.name] = (f.related, None if f.model == self.model else f.model, True, False) + cache[f.name] = cache[f.attname] = ( + f.related, None if f.model == self.model else f.model, True, False) if app_cache_ready(): self._name_map = cache return cache @@ -481,7 +482,7 @@ class Options(object): return [t for t in cache.items() if all(p(*t) for p in predicates)] def _fill_related_objects_cache(self): - cache = SortedDict() + cache = OrderedDict() parent_list = self.get_parent_list() for parent in self.parents: for obj, model in parent._meta.get_all_related_objects_with_model(include_hidden=True): @@ -526,7 +527,7 @@ class Options(object): return list(six.iteritems(cache)) def _fill_related_many_to_many_cache(self): - cache = SortedDict() + cache = OrderedDict() parent_list = self.get_parent_list() for parent in self.parents: for obj, model in parent._meta.get_all_related_m2m_objects_with_model(): diff --git a/django/db/models/query.py b/django/db/models/query.py index 811e917764..4069c04a14 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -8,9 +8,9 @@ import sys from django.conf import settings from django.core import exceptions -from django.db import connections, router, transaction, DatabaseError +from django.db import connections, router, transaction, DatabaseError, IntegrityError from django.db.models.constants import LOOKUP_SEP -from django.db.models.fields import AutoField +from django.db.models.fields import AutoField, Empty from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) from django.db.models.deletion import Collector @@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20 EmptyResultSet = sql.EmptyResultSet +def _pickle_queryset(class_bases, class_dict): + """ + Used by `__reduce__` to create the initial version of the `QuerySet` class + onto which the output of `__getstate__` will be applied. + + See `__reduce__` for more details. + """ + new = Empty() + new.__class__ = type(class_bases[0].__name__, class_bases, class_dict) + return new + + class QuerySet(object): """ Represents a lazy database lookup for a set of objects. """ + def __init__(self, model=None, query=None, using=None): self.model = model self._db = using @@ -45,6 +58,13 @@ class QuerySet(object): self._prefetch_done = False self._known_related_objects = {} # {rel_field, {pk: rel_obj}} + def as_manager(cls): + # Address the circular dependency between `Queryset` and `Manager`. + from django.db.models.manager import Manager + return Manager.from_queryset(cls)() + as_manager.queryset_only = True + as_manager = classmethod(as_manager) + ######################## # PYTHON MAGIC METHODS # ######################## @@ -70,6 +90,26 @@ class QuerySet(object): obj_dict = self.__dict__.copy() return obj_dict + def __reduce__(self): + """ + Used by pickle to deal with the types that we create dynamically when + specialized queryset such as `ValuesQuerySet` are used in conjunction + with querysets that are *subclasses* of `QuerySet`. + + See `_clone` implementation for more details. + """ + if hasattr(self, '_specialized_queryset_class'): + class_bases = ( + self._specialized_queryset_class, + self._base_queryset_class, + ) + class_dict = { + '_specialized_queryset_class': self._specialized_queryset_class, + '_base_queryset_class': self._base_queryset_class, + } + return _pickle_queryset, (class_bases, class_dict), self.__getstate__() + return super(QuerySet, self).__reduce__() + def __repr__(self): data = list(self[:REPR_OUTPUT_SIZE + 1]) if len(data) > REPR_OUTPUT_SIZE: @@ -274,9 +314,11 @@ class QuerySet(object): query = self.query.clone() + aggregate_names = [] for (alias, aggregate_expr) in kwargs.items(): query.add_aggregate(aggregate_expr, self.model, alias, - is_summary=True) + is_summary=True) + aggregate_names.append(alias) return query.get_aggregation(using=self.db) @@ -371,8 +413,8 @@ class QuerySet(object): specifying whether an object was created. """ lookup, params, _ = self._extract_model_params(defaults, **kwargs) + self._for_write = True try: - self._for_write = True return self.get(**lookup), False except self.model.DoesNotExist: return self._create_object_from_params(lookup, params) @@ -385,8 +427,8 @@ class QuerySet(object): specifying whether an object was created. """ lookup, params, filtered_defaults = self._extract_model_params(defaults, **kwargs) + self._for_write = True try: - self._for_write = True obj = self.get(**lookup) except self.model.DoesNotExist: obj, created = self._create_object_from_params(lookup, params) @@ -394,34 +436,36 @@ class QuerySet(object): return obj, created for k, v in six.iteritems(filtered_defaults): setattr(obj, k, v) + + sid = transaction.savepoint(using=self.db) try: - sid = transaction.savepoint(using=self.db) obj.save(update_fields=filtered_defaults.keys(), using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, False except DatabaseError: transaction.savepoint_rollback(sid, using=self.db) - six.reraise(sys.exc_info()) + six.reraise(*sys.exc_info()) def _create_object_from_params(self, lookup, params): """ Tries to create an object using passed params. Used by get_or_create and update_or_create """ + obj = self.model(**params) + sid = transaction.savepoint(using=self.db) try: - obj = self.model(**params) - sid = transaction.savepoint(using=self.db) obj.save(force_insert=True, using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, True - except DatabaseError: + except DatabaseError as e: transaction.savepoint_rollback(sid, using=self.db) exc_info = sys.exc_info() - try: - return self.get(**lookup), False - except self.model.DoesNotExist: - # Re-raise the DatabaseError with its original traceback. - six.reraise(*exc_info) + if isinstance(e, IntegrityError): + try: + return self.get(**lookup), False + except self.model.DoesNotExist: + pass + six.reraise(*exc_info) def _extract_model_params(self, defaults, **kwargs): """ @@ -523,6 +567,7 @@ class QuerySet(object): # Clear the result cache, in case this QuerySet gets reused. self._result_cache = None delete.alters_data = True + delete.queryset_only = True def _raw_delete(self, using): """ @@ -562,6 +607,7 @@ class QuerySet(object): self._result_cache = None return query.get_compiler(self.db).execute_sql(None) _update.alters_data = True + _update.queryset_only = False def exists(self): if self._result_cache is None: @@ -577,6 +623,13 @@ class QuerySet(object): # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # ################################################## + def raw(self, raw_query, params=None, translations=None, using=None): + if using is None: + using = self.db + return RawQuerySet(raw_query, model=self.model, + params=params, translations=translations, + using=using) + def values(self, *fields): return self._clone(klass=ValuesQuerySet, setup=True, _fields=fields) @@ -689,6 +742,7 @@ class QuerySet(object): # Default to false for nowait nowait = kwargs.pop('nowait', False) obj = self._clone() + obj._for_write = True obj.query.select_for_update = True obj.query.select_for_update_nowait = nowait return obj @@ -863,6 +917,21 @@ class QuerySet(object): ################### # PRIVATE METHODS # ################### + + def _insert(self, objs, fields, return_id=False, raw=False, using=None): + """ + Inserts a new record for the given model. This provides an interface to + the InsertQuery class and is how Model.save() is implemented. + """ + self._for_write = True + if using is None: + using = self.db + query = sql.InsertQuery(self.model) + query.insert_values(fields, objs, raw=raw) + return query.get_compiler(using=using).execute_sql(return_id) + _insert.alters_data = True + _insert.queryset_only = False + def _batched_insert(self, objs, fields, batch_size): """ A little helper method for bulk_insert to insert the bulk one batch @@ -881,6 +950,15 @@ class QuerySet(object): def _clone(self, klass=None, setup=False, **kwargs): if klass is None: klass = self.__class__ + elif not issubclass(self.__class__, klass): + base_queryset_class = getattr(self, '_base_queryset_class', self.__class__) + class_bases = (klass, base_queryset_class) + class_dict = { + '_base_queryset_class': base_queryset_class, + '_specialized_queryset_class': klass, + } + klass = type(klass.__name__, class_bases, class_dict) + query = self.query.clone() if self._sticky_filter: query.filter_is_sticky = True @@ -1546,17 +1624,6 @@ class RawQuerySet(object): return self._model_fields -def insert_query(model, objs, fields, return_id=False, raw=False, using=None): - """ - Inserts a new record for the given model. This provides an interface to - the InsertQuery class and is how Model.save() is implemented. It is not - part of the public API. - """ - query = sql.InsertQuery(model) - query.insert_values(fields, objs, raw=raw) - return query.get_compiler(using=using).execute_sql(return_id) - - def prefetch_related_objects(result_cache, related_lookups): """ Helper function for prefetch_related functionality diff --git a/django/db/models/sql/__init__.py b/django/db/models/sql/__init__.py index df5b74e326..8bc60c1d10 100644 --- a/django/db/models/sql/__init__.py +++ b/django/db/models/sql/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.subqueries import * from django.db.models.sql.query import * diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 2a9b8ef826..15f2643495 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -7,9 +7,9 @@ databases). The abstraction barrier only works one way: this module has to know all about the internals of models in order to get the information it needs. """ +from collections import OrderedDict import copy -from django.utils.datastructures import SortedDict from django.utils.encoding import force_text from django.utils.tree import Node from django.utils import six @@ -142,7 +142,7 @@ class Query(object): self.select_related = False # SQL aggregate-related attributes - self.aggregates = SortedDict() # Maps alias -> SQL aggregate function + self.aggregates = OrderedDict() # Maps alias -> SQL aggregate function self.aggregate_select_mask = None self._aggregate_select_cache = None @@ -152,7 +152,7 @@ class Query(object): # These are for extensions. The contents are more or less appended # verbatim to the appropriate clause. - self.extra = SortedDict() # Maps col_alias -> (col_sql, params). + self.extra = OrderedDict() # Maps col_alias -> (col_sql, params). self.extra_select_mask = None self._extra_select_cache = None @@ -321,6 +321,7 @@ class Query(object): # information but retrieves only the first row. Aggregate # over the subquery instead. if self.group_by is not None: + from django.db.models.sql.subqueries import AggregateQuery query = AggregateQuery(self.model) @@ -479,7 +480,7 @@ class Query(object): rhs_used_joins = set(change_map.values()) to_promote = [alias for alias in self.tables if alias not in rhs_used_joins] - self.promote_joins(to_promote, True) + self.promote_joins(to_promote) # Now relabel a copy of the rhs where-clause and add it to the current # one. @@ -658,7 +659,7 @@ class Query(object): """ Decreases the reference count for this alias. """ self.alias_refcount[alias] -= amount - def promote_joins(self, aliases, unconditional=False): + def promote_joins(self, aliases): """ Promotes recursively the join type of given aliases and its children to an outer join. If 'unconditional' is False, the join is only promoted if @@ -681,12 +682,14 @@ class Query(object): # isn't really joined at all in the query, so we should not # alter its join type. continue + # Only the first alias (skipped above) should have None join_type + assert self.alias_map[alias].join_type is not None parent_alias = self.alias_map[alias].lhs_alias parent_louter = (parent_alias and self.alias_map[parent_alias].join_type == self.LOUTER) already_louter = self.alias_map[alias].join_type == self.LOUTER - if ((unconditional or self.alias_map[alias].nullable - or parent_louter) and not already_louter): + if ((self.alias_map[alias].nullable or parent_louter) and + not already_louter): data = self.alias_map[alias]._replace(join_type=self.LOUTER) self.alias_map[alias] = data # Join type of 'alias' changed, so re-examine all aliases that @@ -740,7 +743,7 @@ class Query(object): self.group_by = [relabel_column(col) for col in self.group_by] self.select = [SelectInfo(relabel_column(s.col), s.field) for s in self.select] - self.aggregates = SortedDict( + self.aggregates = OrderedDict( (key, relabel_column(col)) for key, col in self.aggregates.items()) # 2. Rename the alias in the internal table/alias datastructures. @@ -794,7 +797,7 @@ class Query(object): assert current < ord('Z') prefix = chr(current + 1) self.alias_prefix = prefix - change_map = SortedDict() + change_map = OrderedDict() for pos, alias in enumerate(self.tables): if alias in exceptions: continue @@ -985,7 +988,7 @@ class Query(object): # If the aggregate references a model or field that requires a join, # those joins must be LEFT OUTER - empty join rows must be returned # in order for zeros to be returned for those aggregates. - self.promote_joins(join_list, True) + self.promote_joins(join_list) col = targets[0].column source = sources[0] @@ -996,6 +999,8 @@ class Query(object): field_name = field_list[0] source = opts.get_field(field_name) col = field_name + # We want to have the alias in SELECT clause even if mask is set. + self.append_aggregate_mask([alias]) # Add the aggregate to the query aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary) @@ -1091,8 +1096,7 @@ class Query(object): try: field, sources, opts, join_list, path = self.setup_joins( - parts, opts, alias, can_reuse, allow_many, - allow_explicit_fk=True) + parts, opts, alias, can_reuse, allow_many,) if can_reuse is not None: can_reuse.update(join_list) except MultiJoin as e: @@ -1237,15 +1241,14 @@ class Query(object): len(q_object.children)) return target_clause - def names_to_path(self, names, opts, allow_many, allow_explicit_fk): + def names_to_path(self, names, opts, allow_many): """ Walks the names path and turns them PathInfo tuples. Note that a single name in 'names' can generate multiple PathInfos (m2m for example). 'names' is the path of names to travle, 'opts' is the model Options we - start the name resolving from, 'allow_many' and 'allow_explicit_fk' - are as for setup_joins(). + start the name resolving from, 'allow_many' is as for setup_joins(). Returns a list of PathInfo tuples. In addition returns the final field (the last used join field), and target (which is a field guaranteed to @@ -1258,17 +1261,9 @@ class Query(object): try: field, model, direct, m2m = opts.get_field_by_name(name) except FieldDoesNotExist: - for f in opts.fields: - if allow_explicit_fk and name == f.attname: - # XXX: A hack to allow foo_id to work in values() for - # backwards compatibility purposes. If we dropped that - # feature, this could be removed. - field, model, direct, m2m = opts.get_field_by_name(f.name) - break - else: - available = opts.get_all_field_names() + list(self.aggregate_select) - raise FieldError("Cannot resolve keyword %r into field. " - "Choices are: %s" % (name, ", ".join(available))) + available = opts.get_all_field_names() + list(self.aggregate_select) + raise FieldError("Cannot resolve keyword %r into field. " + "Choices are: %s" % (name, ", ".join(available))) # Check if we need any joins for concrete inheritance cases (the # field lives in parent, but we are currently in one of its # children) @@ -1314,7 +1309,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, outer_if_first=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 @@ -1329,9 +1324,6 @@ class Query(object): If 'allow_many' is False, then any reverse foreign key seen will generate a MultiJoin exception. - The 'allow_explicit_fk' controls if field.attname is allowed in the - lookups. - Returns the final field involved in the joins, the target field (used for any 'where' constraint), the final 'opts' value, the joins and the field path travelled to generate the joins. @@ -1345,7 +1337,7 @@ class Query(object): joins = [alias] # First, generate the path for the names path, final_field, targets = self.names_to_path( - names, opts, allow_many, allow_explicit_fk) + names, opts, allow_many) # Then, add the path to the query's joins. Note that we can't trim # joins at this stage - we will need the information about join type # of the trimmed joins. @@ -1648,7 +1640,7 @@ class Query(object): # dictionary with their parameters in 'select_params' so that # subsequent updates to the select dictionary also adjust the # parameters appropriately. - select_pairs = SortedDict() + select_pairs = OrderedDict() if select_params: param_iter = iter(select_params) else: @@ -1661,7 +1653,7 @@ class Query(object): entry_params.append(next(param_iter)) pos = entry.find("%s", pos + 2) select_pairs[name] = (entry, entry_params) - # This is order preserving, since self.extra_select is a SortedDict. + # This is order preserving, since self.extra_select is an OrderedDict. self.extra.update(select_pairs) if where or params: self.where.add(ExtraWhere(where, params), AND) @@ -1753,6 +1745,10 @@ class Query(object): self.aggregate_select_mask = set(names) self._aggregate_select_cache = None + def append_aggregate_mask(self, names): + if self.aggregate_select_mask is not None: + self.set_aggregate_mask(set(names).union(self.aggregate_select_mask)) + def set_extra_mask(self, names): """ Set the mask of extra select items that will be returned by SELECT, @@ -1766,7 +1762,7 @@ class Query(object): self._extra_select_cache = None def _aggregate_select(self): - """The SortedDict of aggregate columns that are not masked, and should + """The OrderedDict of aggregate columns that are not masked, and should be used in the SELECT clause. This result is cached for optimization purposes. @@ -1774,7 +1770,7 @@ class Query(object): if self._aggregate_select_cache is not None: return self._aggregate_select_cache elif self.aggregate_select_mask is not None: - self._aggregate_select_cache = SortedDict([ + self._aggregate_select_cache = OrderedDict([ (k, v) for k, v in self.aggregates.items() if k in self.aggregate_select_mask ]) @@ -1787,7 +1783,7 @@ class Query(object): if self._extra_select_cache is not None: return self._extra_select_cache elif self.extra_select_mask is not None: - self._extra_select_cache = SortedDict([ + self._extra_select_cache = OrderedDict([ (k, v) for k, v in self.extra.items() if k in self.extra_select_mask ]) diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 6aab02bd9a..8beb3fa74a 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -11,8 +11,6 @@ from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, SelectInfo from django.db.models.sql.datastructures import Date, DateTime from django.db.models.sql.query import Query from django.db.models.sql.where import AND, Constraint -from django.utils.functional import Promise -from django.utils.encoding import force_text from django.utils import six from django.utils import timezone @@ -147,10 +145,6 @@ class UpdateQuery(Query): Used by add_update_values() as well as the "fast" update path when saving models. """ - # Check that no Promise object passes to the query. Refs #10498. - values_seq = [(value[0], value[1], force_text(value[2])) - if isinstance(value[2], Promise) else value - for value in values_seq] self.values.extend(values_seq) def add_related_update(self, model, field, value): @@ -210,12 +204,6 @@ class InsertQuery(Query): into the query, for example. """ self.fields = fields - # Check that no Promise object reaches the DB. Refs #10498. - for field in fields: - for obj in objs: - value = getattr(obj, field.attname) - if isinstance(value, Promise): - setattr(obj, field.attname, force_text(value)) self.objs = objs self.raw = raw diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 2e83ecdce4..7b71580370 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -2,8 +2,6 @@ Code to manage the creation and SQL rendering of 'where' constraints. """ -from __future__ import absolute_import - import collections import datetime from itertools import repeat diff --git a/django/db/utils.py b/django/db/utils.py index c1bce7326b..bcfb06f584 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -1,4 +1,5 @@ from functools import wraps +from importlib import import_module import os import pkgutil from threading import local @@ -7,7 +8,6 @@ import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.functional import cached_property -from django.utils.importlib import import_module from django.utils.module_loading import import_by_path from django.utils._os import upath from django.utils import six @@ -104,7 +104,7 @@ class DatabaseErrorWrapper(object): def load_backend(backend_name): # Look for a fully qualified database backend name try: - return import_module('.base', backend_name) + return import_module('%s.base' % backend_name) except ImportError as e_user: # The database backend wasn't found. Display a helpful error message # listing all possible (built-in) database backends. diff --git a/django/forms/__init__.py b/django/forms/__init__.py index 2588098330..34896d948d 100644 --- a/django/forms/__init__.py +++ b/django/forms/__init__.py @@ -2,8 +2,6 @@ Django validation and HTML form handling. """ -from __future__ import absolute_import - from django.core.exceptions import ValidationError from django.forms.fields import * from django.forms.forms import * diff --git a/django/forms/extras/__init__.py b/django/forms/extras/__init__.py index d801e4fa80..28316f472c 100644 --- a/django/forms/extras/__init__.py +++ b/django/forms/extras/__init__.py @@ -1,3 +1 @@ -from __future__ import absolute_import - from django.forms.extras.widgets import * diff --git a/django/forms/fields.py b/django/forms/fields.py index a794c02e9f..e995187682 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -2,7 +2,7 @@ Field classes. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import copy import datetime @@ -118,6 +118,8 @@ class Field(object): super(Field, self).__init__() def prepare_value(self, value): + if self.widget.is_localized: + value = formats.localize_input(value) return value def to_python(self, value): @@ -460,6 +462,7 @@ class DateTimeField(BaseTemporalField): } def prepare_value(self, value): + value = super(DateTimeField, self).prepare_value(value) if isinstance(value, datetime.datetime): value = to_current_timezone(value) return value @@ -952,15 +955,20 @@ class MultiValueField(Field): """ default_error_messages = { 'invalid': _('Enter a list of values.'), + 'incomplete': _('Enter a complete value.'), } def __init__(self, fields=(), *args, **kwargs): + self.require_all_fields = kwargs.pop('require_all_fields', True) super(MultiValueField, self).__init__(*args, **kwargs) - # Set 'required' to False on the individual fields, because the - # required validation will be handled by MultiValueField, not by those - # individual fields. for f in fields: - f.required = False + f.error_messages.setdefault('incomplete', + self.error_messages['incomplete']) + if self.require_all_fields: + # Set 'required' to False on the individual fields, because the + # required validation will be handled by MultiValueField, not + # by those individual fields. + f.required = False self.fields = fields def validate(self, value): @@ -990,15 +998,26 @@ class MultiValueField(Field): 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'], code='required') + if field_value in self.empty_values: + if self.require_all_fields: + # Raise a 'required' error if the MultiValueField is + # required and any field is empty. + if self.required: + raise ValidationError(self.error_messages['required'], code='required') + elif field.required: + # Otherwise, add an 'incomplete' error to the list of + # collected errors and skip field cleaning, if a required + # field is empty. + if field.error_messages['incomplete'] not in errors: + errors.append(field.error_messages['incomplete']) + continue 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.error_list) + # exception for the first error we encounter. Skip duplicates. + errors.extend(m for m in e.error_list if m not in errors) if errors: raise ValidationError(errors) diff --git a/django/forms/forms.py b/django/forms/forms.py index e144eb60f8..c2b700ce77 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -2,8 +2,9 @@ Form classes """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals +from collections import OrderedDict import copy import warnings @@ -11,7 +12,6 @@ from django.core.exceptions import ValidationError from django.forms.fields import Field, FileField from django.forms.util import flatatt, ErrorDict, ErrorList from django.forms.widgets import Media, media_property, TextInput, Textarea -from django.utils.datastructures import SortedDict from django.utils.html import conditional_escape, format_html from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe @@ -55,7 +55,7 @@ def get_declared_fields(bases, attrs, with_base_fields=True): if hasattr(base, 'declared_fields'): fields = list(six.iteritems(base.declared_fields)) + fields - return SortedDict(fields) + return OrderedDict(fields) class DeclarativeFieldsMetaclass(type): """ @@ -297,9 +297,12 @@ class BaseForm(object): def _clean_form(self): try: - self.cleaned_data = self.clean() + cleaned_data = self.clean() except ValidationError as e: self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages) + else: + if cleaned_data is not None: + self.cleaned_data = cleaned_data def _post_clean(self): """ @@ -509,20 +512,23 @@ class BoundField(object): ) return self.field.prepare_value(data) - def label_tag(self, contents=None, attrs=None): + def label_tag(self, contents=None, attrs=None, label_suffix=None): """ Wraps the given contents in a <label>, if the field has an ID attribute. contents should be 'mark_safe'd to avoid HTML escaping. If contents aren't given, uses the field's HTML-escaped label. If attrs are given, they're used as HTML attributes on the <label> tag. + + label_suffix allows overriding the form's label_suffix. """ contents = contents or self.label # Only add the suffix if the label does not end in punctuation. # Translators: If found as last label character, these punctuation # characters will prevent the default label_suffix to be appended to the label - if self.form.label_suffix and contents and contents[-1] not in _(':?.!'): - contents = format_html('{0}{1}', contents, self.form.label_suffix) + label_suffix = label_suffix if label_suffix is not None else self.form.label_suffix + if label_suffix and contents and contents[-1] not in _(':?.!'): + contents = format_html('{0}{1}', contents, 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 cb3126e6d7..8a379ff3cf 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.core.exceptions import ValidationError from django.forms import Form diff --git a/django/forms/models.py b/django/forms/models.py index 1d3bec1635..a5b82e521d 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -3,8 +3,9 @@ Helper functions for creating Form classes from Django models and database field objects. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals +from collections import OrderedDict import warnings from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError @@ -15,7 +16,6 @@ from django.forms.util import ErrorList from django.forms.widgets import (SelectMultiple, HiddenInput, MultipleHiddenInput, media_property, CheckboxSelectMultiple) from django.utils.encoding import smart_text, force_text -from django.utils.datastructures import SortedDict from django.utils import six from django.utils.text import get_text_list, capfirst from django.utils.translation import ugettext_lazy as _, ugettext, string_concat @@ -142,7 +142,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None, labels=None, help_texts=None, error_messages=None): """ - Returns a ``SortedDict`` containing form fields for the given model. + Returns a ``OrderedDict`` containing form fields for the given model. ``fields`` is an optional list of field names. If provided, only the named fields will be included in the returned fields. @@ -199,9 +199,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, field_list.append((f.name, formfield)) else: ignored.append(f.name) - field_dict = SortedDict(field_list) + field_dict = OrderedDict(field_list) if fields: - field_dict = SortedDict( + field_dict = OrderedDict( [(f, field_dict.get(f)) for f in fields if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored)] ) @@ -717,7 +717,8 @@ class BaseModelFormSet(BaseFormSet): obj = self._existing_object(pk_value) if form in forms_to_delete: self.deleted_objects.append(obj) - obj.delete() + if commit: + obj.delete() continue if form.has_changed(): self.changed_objects.append((obj, form.changed_data)) diff --git a/django/forms/widgets.py b/django/forms/widgets.py index a92e5a56ce..0a5059a9c2 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -2,7 +2,7 @@ HTML Widget classes """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import copy from itertools import chain @@ -511,6 +511,8 @@ class Select(Widget): return mark_safe('\n'.join(output)) def render_option(self, selected_choices, option_value, option_label): + if option_value == None: + option_value = '' option_value = force_text(option_value) if option_value in selected_choices: selected_html = mark_safe(' selected="selected"') @@ -838,6 +840,11 @@ class MultiWidget(Widget): obj.widgets = copy.deepcopy(self.widgets) return obj + @property + def needs_multipart_form(self): + return any(w.needs_multipart_form for w in self.widgets) + + class SplitDateTimeWidget(MultiWidget): """ A Widget that splits datetime input into two <input type="text"> boxes. diff --git a/django/http/cookie.py b/django/http/cookie.py index 50ff549caf..62d5c61a4d 100644 --- a/django/http/cookie.py +++ b/django/http/cookie.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.utils.encoding import force_str from django.utils import six diff --git a/django/http/request.py b/django/http/request.py index b7f9d241a7..9f9f32b1b4 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import copy import os @@ -68,14 +68,19 @@ class HttpRequest(object): if server_port != ('443' if self.is_secure() else '80'): host = '%s:%s' % (host, server_port) - allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS + # There is no hostname validation when DEBUG=True + if settings.DEBUG: + return host + domain, port = split_domain_port(host) - if domain and validate_host(domain, allowed_hosts): + if domain and validate_host(domain, settings.ALLOWED_HOSTS): return host else: msg = "Invalid HTTP_HOST header: %r." % host if domain: msg += "You may need to add %r to ALLOWED_HOSTS." % domain + else: + msg += "The domain name provided is not valid according to RFC 1034/1035" raise DisallowedHost(msg) def get_full_path(self): diff --git a/django/http/response.py b/django/http/response.py index 1f0849c0bf..aaebefe022 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import time @@ -14,7 +14,7 @@ from django.core import signing from django.core.exceptions import DisallowedRedirect from django.http.cookie import SimpleCookie from django.utils import six, timezone -from django.utils.encoding import force_bytes, iri_to_uri +from django.utils.encoding import force_bytes, force_text, iri_to_uri from django.utils.http import cookie_date from django.utils.six.moves import map @@ -393,7 +393,7 @@ class HttpResponseRedirectBase(HttpResponse): allowed_schemes = ['http', 'https', 'ftp'] def __init__(self, redirect_to, *args, **kwargs): - parsed = urlparse(redirect_to) + parsed = urlparse(force_text(redirect_to)) if parsed.scheme and parsed.scheme not in self.allowed_schemes: raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 4e0a4753ce..cd5d2cce71 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -1,12 +1,13 @@ "This is the locale selecting middleware that will look at accept headers" +from collections import OrderedDict + from django.conf import settings from django.core.urlresolvers import (is_valid_path, get_resolver, LocaleRegexURLResolver) from django.http import HttpResponseRedirect from django.utils.cache import patch_vary_headers from django.utils import translation -from django.utils.datastructures import SortedDict class LocaleMiddleware(object): @@ -19,7 +20,7 @@ class LocaleMiddleware(object): """ def __init__(self): - self._supported_languages = SortedDict(settings.LANGUAGES) + self._supported_languages = OrderedDict(settings.LANGUAGES) self._is_language_prefix_patterns_used = False for url_pattern in get_resolver(None).url_patterns: if isinstance(url_pattern, LocaleRegexURLResolver): diff --git a/django/template/base.py b/django/template/base.py index 364b428070..ed4196012a 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -1,13 +1,13 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import re from functools import partial +from importlib import import_module from inspect import getargspec from django.conf import settings from django.template.context import (Context, RequestContext, ContextPopException) -from django.utils.importlib import import_module from django.utils.itercompat import is_iterable from django.utils.text import (smart_split, unescape_string_literal, get_text_list) diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index cea01558d9..b25f14aace 100644 --- a/django/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -3,6 +3,7 @@ Wrapper for loading templates from "templates" directories in INSTALLED_APPS packages. """ +from importlib import import_module import os import sys @@ -11,7 +12,6 @@ from django.core.exceptions import ImproperlyConfigured from django.template.base import TemplateDoesNotExist from django.template.loader import BaseLoader from django.utils._os import safe_join -from django.utils.importlib import import_module from django.utils import six # At compile time, cache the directories to search. diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py index c61045de06..b33b4e8446 100644 --- a/django/template/loaders/cached.py +++ b/django/template/loaders/cached.py @@ -13,6 +13,7 @@ class Loader(BaseLoader): def __init__(self, loaders): self.template_cache = {} + self.find_template_cache = {} self._loaders = loaders self._cached_loaders = [] @@ -28,21 +29,34 @@ class Loader(BaseLoader): self._cached_loaders = cached_loaders return self._cached_loaders - def find_template(self, name, dirs=None): - for loader in self.loaders: - try: - template, display_name = loader(name, dirs) - return (template, make_origin(display_name, loader, name, dirs)) - except TemplateDoesNotExist: - pass - raise TemplateDoesNotExist(name) - - def load_template(self, template_name, template_dirs=None): - key = template_name + def cache_key(self, template_name, template_dirs): if template_dirs: # If template directories were specified, use a hash to differentiate - key = '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()]) + return '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()]) + else: + return template_name + + def find_template(self, name, dirs=None): + key = self.cache_key(name, dirs) + try: + result = self.find_template_cache[key] + except KeyError: + result = None + for loader in self.loaders: + try: + template, display_name = loader(name, dirs) + except TemplateDoesNotExist: + pass + else: + result = (template, make_origin(display_name, loader, name, dirs)) + self.find_template_cache[key] = result + if result: + return result + else: + raise TemplateDoesNotExist(name) + def load_template(self, template_name, template_dirs=None): + key = self.cache_key(template_name, template_dirs) try: template = self.template_cache[key] except KeyError: @@ -62,3 +76,4 @@ class Loader(BaseLoader): def reset(self): "Empty the template cache." self.template_cache.clear() + self.find_template_cache.clear() diff --git a/django/test/client.py b/django/test/client.py index 8dbd33ccf1..754d4d73f8 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -5,6 +5,7 @@ import os import re import mimetypes from copy import copy +from importlib import import_module from io import BytesIO try: from urllib.parse import unquote, urlparse, urlsplit @@ -25,7 +26,6 @@ from django.test import signals from django.utils.functional import curry from django.utils.encoding import force_bytes, force_str from django.utils.http import urlencode -from django.utils.importlib import import_module from django.utils.itercompat import is_iterable from django.utils import six from django.test.utils import ContextList diff --git a/django/test/simple.py b/django/test/simple.py index 019eff2421..b2ab1269b5 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -3,6 +3,7 @@ This module is pending deprecation as of Django 1.6 and will be removed in version 1.8. """ +from importlib import import_module import json import re import unittest as real_unittest @@ -15,7 +16,6 @@ from django.test.utils import compare_xml, strip_quotes # django.utils.unittest is deprecated, but so is django.test.simple, # and the latter will be removed before the former. from django.utils import unittest -from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule __all__ = ('DjangoTestSuiteRunner',) diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 20b771e95b..706c09593b 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -1,7 +1,7 @@ import copy +import warnings from django.utils import six - class MergeDict(object): """ A simple class for creating new "virtual" dictionaries that actually look @@ -124,6 +124,10 @@ class SortedDict(dict): return instance def __init__(self, data=None): + warnings.warn( + "SortedDict is deprecated and will be removed in Django 1.9.", + PendingDeprecationWarning, stacklevel=2 + ) if data is None or isinstance(data, dict): data = data or [] super(SortedDict, self).__init__(data) diff --git a/django/utils/formats.py b/django/utils/formats.py index cc66149026..6b89b40ecd 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -1,10 +1,10 @@ import decimal import datetime +from importlib import import_module import unicodedata from django.conf import settings from django.utils import dateformat, numberformat, datetime_safe -from django.utils.importlib import import_module from django.utils.encoding import force_str from django.utils.functional import lazy from django.utils.safestring import mark_safe @@ -54,7 +54,7 @@ def iter_format_modules(lang): for location in format_locations: for loc in locales: try: - yield import_module('.formats', location % loc) + yield import_module('%s.formats' % (location % loc)) except ImportError: pass diff --git a/django/utils/html.py b/django/utils/html.py index 4893b6b18a..89e790d96f 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -4,13 +4,13 @@ from __future__ import unicode_literals import re try: - from urllib.parse import quote, urlsplit, urlunsplit + from urllib.parse import quote, unquote, urlsplit, urlunsplit except ImportError: # Python 2 - from urllib import quote + from urllib import quote, unquote from urlparse import urlsplit, urlunsplit from django.utils.safestring import SafeData, mark_safe -from django.utils.encoding import force_bytes, force_text +from django.utils.encoding import force_text, force_str from django.utils.functional import allow_lazy from django.utils import six from django.utils.text import normalize_newlines @@ -26,7 +26,6 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>')] DOTS = ['·', '*', '\u2022', '•', '•', '•'] unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)') -unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})') word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) @@ -185,11 +184,9 @@ def smart_urlquote(url): # invalid IPv6 URL (normally square brackets in hostname part). pass - # An URL is considered unquoted if it contains no % characters or - # contains a % not followed by two hexadecimal digits. See #9655. - if '%' not in url or unquoted_percents_re.search(url): - # See http://bugs.python.org/issue2637 - url = quote(force_bytes(url), safe=b'!*\'();:@&=+$,/?#[]~') + url = unquote(force_str(url)) + # See http://bugs.python.org/issue2637 + url = quote(url, safe=b'!*\'();:@&=+$,/?#[]~') return force_text(url) diff --git a/django/utils/importlib.py b/django/utils/importlib.py index 703ba7f6d8..26e45589e3 100644 --- a/django/utils/importlib.py +++ b/django/utils/importlib.py @@ -1,6 +1,11 @@ # Taken from Python 2.7 with permission from/by the original author. +import warnings import sys +warnings.warn("django.utils.importlib will be removed in Django 1.9.", + PendingDeprecationWarning, stacklevel=2) + + def _resolve_name(name, package, level): """Return the absolute name of the module to be imported.""" if not hasattr(package, 'rindex'): diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index ede585e562..359982e6ba 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -1,10 +1,10 @@ import imp +from importlib import import_module import os import sys from django.core.exceptions import ImproperlyConfigured from django.utils import six -from django.utils.importlib import import_module def import_by_path(dotted_path, error_prefix=''): diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 195badfc00..11463d8f9b 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -1,16 +1,16 @@ """Translation helper functions.""" from __future__ import unicode_literals +from collections import OrderedDict import locale import os import re import sys import gettext as gettext_module +from importlib import import_module from threading import local import warnings -from django.utils.importlib import import_module -from django.utils.datastructures import SortedDict from django.utils.encoding import force_str, force_text from django.utils.functional import memoize from django.utils._os import upath @@ -369,7 +369,7 @@ def get_supported_language_variant(lang_code, supported=None, strict=False): """ if supported is None: from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) if lang_code: # if fr-CA is not supported, try fr-ca; if that fails, fallback to fr. generic_lang_code = lang_code.split('-')[0] @@ -396,7 +396,7 @@ def get_language_from_path(path, supported=None, strict=False): """ if supported is None: from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) regex_match = language_code_prefix_re.match(path) if not regex_match: return None @@ -418,7 +418,7 @@ def get_language_from_request(request, check_path=False): """ global _accepted from django.conf import settings - supported = SortedDict(settings.LANGUAGES) + supported = OrderedDict(settings.LANGUAGES) if check_path: lang_code = get_language_from_path(request.path_info, supported) diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py index 78ae6b1442..a611981e79 100644 --- a/django/views/decorators/debug.py +++ b/django/views/decorators/debug.py @@ -1,5 +1,7 @@ import functools +from django.http import HttpRequest + def sensitive_variables(*variables): """ @@ -62,6 +64,10 @@ def sensitive_post_parameters(*parameters): def decorator(view): @functools.wraps(view) def sensitive_post_parameters_wrapper(request, *args, **kwargs): + assert isinstance(request, HttpRequest), ( + "sensitive_post_parameters didn't receive an HttpRequest. If you " + "are decorating a classmethod, be sure to use @method_decorator." + ) if parameters: request.sensitive_post_parameters = parameters else: diff --git a/django/views/defaults.py b/django/views/defaults.py index e9d0f16a33..2f54fcd263 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -21,11 +21,14 @@ def page_not_found(request, template_name='404.html'): """ try: template = loader.get_template(template_name) + content_type = None # Django will use DEFAULT_CONTENT_TYPE except TemplateDoesNotExist: template = Template( '<h1>Not Found</h1>' '<p>The requested URL {{ request_path }} was not found on this server.</p>') - return http.HttpResponseNotFound(template.render(RequestContext(request, {'request_path': request.path}))) + content_type = 'text/html' + body = template.render(RequestContext(request, {'request_path': request.path})) + return http.HttpResponseNotFound(body, content_type=content_type) @requires_csrf_token @@ -39,7 +42,7 @@ def server_error(request, template_name='500.html'): try: template = loader.get_template(template_name) except TemplateDoesNotExist: - return http.HttpResponseServerError('<h1>Server Error (500)</h1>') + return http.HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html') return http.HttpResponseServerError(template.render(Context({}))) @@ -54,7 +57,7 @@ def bad_request(request, template_name='400.html'): try: template = loader.get_template(template_name) except TemplateDoesNotExist: - return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>') + return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>', content_type='text/html') return http.HttpResponseBadRequest(template.render(Context({}))) @@ -75,7 +78,7 @@ def permission_denied(request, template_name='403.html'): try: template = loader.get_template(template_name) except TemplateDoesNotExist: - return http.HttpResponseForbidden('<h1>403 Forbidden</h1>') + return http.HttpResponseForbidden('<h1>403 Forbidden</h1>', content_type='text/html') return http.HttpResponseForbidden(template.render(RequestContext(request))) diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index 23000641b4..5ce8092c67 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -57,19 +57,23 @@ class SingleObjectMixin(ContextMixin): def get_queryset(self): """ - Get the queryset to look an object up against. May not be called if - `get_object` is overridden. + Return the `QuerySet` that will be used to look up the object. + + Note that this method is called by the default implementation of + `get_object` and may not be called if `get_object` is overriden. """ if self.queryset is None: if self.model: return self.model._default_manager.all() else: - raise ImproperlyConfigured("%(cls)s is missing a queryset. Define " - "%(cls)s.model, %(cls)s.queryset, or override " - "%(cls)s.get_queryset()." % { - 'cls': self.__class__.__name__ - }) - return self.queryset._clone() + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset()." % { + 'cls': self.__class__.__name__ + } + ) + return self.queryset.all() def get_slug_field(self): """ diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 1aff3454f4..7381fd1be9 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.core.paginator import Paginator, InvalidPage from django.core.exceptions import ImproperlyConfigured +from django.db.models.query import QuerySet from django.http import Http404 from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateResponseMixin, ContextMixin, View @@ -22,18 +23,25 @@ class MultipleObjectMixin(ContextMixin): def get_queryset(self): """ - Get the list of items for this view. This must be an iterable, and may - be a queryset (in which qs-specific behavior will be enabled). + Return the list of items for this view. + + The return value must be an iterable and may be an instance of + `QuerySet` in which case `QuerySet` specific behavior will be enabled. """ if self.queryset is not None: queryset = self.queryset - if hasattr(queryset, '_clone'): - queryset = queryset._clone() + if isinstance(queryset, QuerySet): + queryset = queryset.all() elif self.model is not None: queryset = self.model._default_manager.all() else: - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % self.__class__.__name__) + raise ImproperlyConfigured( + "%(cls)s is missing a QuerySet. Define " + "%(cls)s.model, %(cls)s.queryset, or override " + "%(cls)s.get_queryset()." % { + 'cls': self.__class__.__name__ + } + ) return queryset def paginate_queryset(self, queryset, page_size): diff --git a/django/views/i18n.py b/django/views/i18n.py index 71ac005855..c913922999 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -1,3 +1,4 @@ +import importlib import json import os import gettext as gettext_module @@ -5,7 +6,6 @@ import gettext as gettext_module from django import http from django.conf import settings from django.template import Context, Template -from django.utils import importlib from django.utils.translation import check_for_language, to_locale, get_language from django.utils.encoding import smart_text from django.utils.formats import get_format_modules, get_format diff --git a/docs/Makefile b/docs/Makefile index a2c926c7ea..21b8a9cbe9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,11 +2,11 @@ # # You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build -LANGUAGE = +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +PAPER ?= +BUILDDIR ?= _build +LANGUAGE ?= # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index 833232a0c3..e7e316f58b 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -92,9 +92,15 @@ class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): # Don't use border=1, which docutils does by default. def visit_table(self, node): + self.context.append(self.compact_p) + self.compact_p = True self._table_row_index = 0 # Needed by Sphinx self.body.append(self.starttag(node, 'table', CLASS='docutils')) + def depart_table(self, node): + self.compact_p = self.context.pop() + self.body.append('</table>\n') + # <big>? Really? def visit_desc_parameterlist(self, node): self.body.append('(') diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 34e68d3700..2325e32ac2 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -193,7 +193,7 @@ Attributes ---------- All attributes can be set in your derived class and can be used in -:class:`BaseCommand`'s :ref:`subclasses<ref-basecommand-subclasses>`. +:class:`BaseCommand`’s :ref:`subclasses<ref-basecommand-subclasses>`. .. attribute:: BaseCommand.args @@ -245,7 +245,7 @@ All attributes can be set in your derived class and can be used in Make sure you know what you are doing if you decide to change the value of this option in your custom command if it creates database content that is locale-sensitive and such content shouldn't contain any translations (like - it happens e.g. with django.contrim.auth permissions) as making the locale + it happens e.g. with django.contrib.auth permissions) as making the locale differ from the de facto default 'en-us' might cause unintended effects. See the `Management commands and locales`_ section above for further details. @@ -267,7 +267,7 @@ the :meth:`~BaseCommand.handle` method must be implemented. .. admonition:: Implementing a constructor in a subclass If you implement ``__init__`` in your subclass of :class:`BaseCommand`, - you must call :class:`BaseCommand`'s ``__init__``. + you must call :class:`BaseCommand`’s ``__init__``. .. code-block:: python diff --git a/docs/howto/deployment/checklist.txt b/docs/howto/deployment/checklist.txt index 7d382401a0..36c442fbe2 100644 --- a/docs/howto/deployment/checklist.txt +++ b/docs/howto/deployment/checklist.txt @@ -206,6 +206,21 @@ See :doc:`/howto/error-reporting` for details on error reporting by email. .. _Sentry: http://sentry.readthedocs.org/en/latest/ +Customize the default error views +--------------------------------- + +Django includes default views and templates for several HTTP error codes. You +may want to override the default templates by creating the following templates +in your root template directory: ``404.html``, ``500.html``, ``403.html``, and +``400.html``. The default views should suffice for 99% of Web applications, but +if you desire to customize them, see these instructions which also contain +details about the default templates: + +* :ref:`http_not_found_view` +* :ref:`http_internal_server_error_view` +* :ref:`http_forbidden_view` +* :ref:`http_bad_request_view` + Miscellaneous ============= diff --git a/docs/howto/deployment/fastcgi.txt b/docs/howto/deployment/fastcgi.txt index 507e50d1a2..cc8d00966c 100644 --- a/docs/howto/deployment/fastcgi.txt +++ b/docs/howto/deployment/fastcgi.txt @@ -2,6 +2,9 @@ How to use Django with FastCGI, SCGI, or AJP ============================================ +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + .. highlight:: bash Although :doc:`WSGI</howto/deployment/wsgi/index>` is the preferred deployment diff --git a/docs/howto/deployment/index.txt b/docs/howto/deployment/index.txt index ed4bcf3d4a..8b0368ac67 100644 --- a/docs/howto/deployment/index.txt +++ b/docs/howto/deployment/index.txt @@ -10,9 +10,15 @@ ways to easily deploy Django: :maxdepth: 1 wsgi/index - fastcgi checklist +FastCGI support is deprecated and will be removed in Django 1.9. + +.. toctree:: + :maxdepth: 1 + + fastcgi + If you're new to deploying Django and/or Python, we'd recommend you try :doc:`mod_wsgi </howto/deployment/wsgi/modwsgi>` first. In most cases it'll be the easiest, fastest, and most stable deployment choice. diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 987a503e95..5ebed92187 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -120,8 +120,8 @@ Filtering sensitive information Error reports are really helpful for debugging errors, so it is generally useful to record as much relevant information about those errors as possible. For example, by default Django records the `full traceback`_ for the -exception raised, each `traceback frame`_'s local variables, and the -:class:`~django.http.HttpRequest`'s :ref:`attributes<httprequest-attributes>`. +exception raised, each `traceback frame`_’s local variables, and the +:class:`~django.http.HttpRequest`’s :ref:`attributes<httprequest-attributes>`. However, sometimes certain types of information may be too sensitive and thus may not be appropriate to be kept track of, for example a user's password or @@ -164,7 +164,7 @@ production environment (that is, where :setting:`DEBUG` is set to ``False``): .. admonition:: When using mutiple decorators If the variable you want to hide is also a function argument (e.g. - '``user``' in the following example), and if the decorated function has + '``user``’ in the following example), and if the decorated function has mutiple decorators, then make sure to place ``@sensitive_variables`` at the top of the decorator chain. This way it will also hide the function argument as it gets passed through the other decorators:: @@ -232,7 +232,7 @@ own filter class and tell Django to use it via the DEFAULT_EXCEPTION_REPORTER_FILTER = 'path.to.your.CustomExceptionReporterFilter' You may also control in a more granular way which filter to use within any -given view by setting the ``HttpRequest``'s ``exception_reporter_filter`` +given view by setting the ``HttpRequest``’s ``exception_reporter_filter`` attribute:: def my_view(request): diff --git a/docs/howto/initial-data.txt b/docs/howto/initial-data.txt index cea07bfea3..b86aaa834e 100644 --- a/docs/howto/initial-data.txt +++ b/docs/howto/initial-data.txt @@ -142,7 +142,7 @@ database tables already will have been created. If you require data for a test case, you should add it using either a :ref:`test fixture <topics-testing-fixtures>`, or - programatically add it during the ``setUp()`` of your test case. + programmatically add it during the ``setUp()`` of your test case. Database-backend-specific SQL data ---------------------------------- diff --git a/docs/index.txt b/docs/index.txt index 19d574cb26..c58be5fcfc 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -193,7 +193,7 @@ testing of Django applications: * **Deployment:** :doc:`Overview <howto/deployment/index>` | :doc:`WSGI servers <howto/deployment/wsgi/index>` | - :doc:`FastCGI/SCGI/AJP <howto/deployment/fastcgi>` | + :doc:`FastCGI/SCGI/AJP <howto/deployment/fastcgi>` (deprecated) | :doc:`Deploying static files <howto/static-files/deployment>` | :doc:`Tracking code errors by email <howto/error-reporting>` diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 7ee65cd6fa..b38c6f6783 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -25,16 +25,26 @@ Quickstart ~~~~~~~~~~ Running the tests requires a Django settings module that defines the -databases to use. To make it easy to get started, Django provides a -sample settings module that uses the SQLite database. To run the tests -with this sample ``settings`` module: +databases to use. To make it easy to get started, Django provides and uses a +sample settings module that uses the SQLite database. To run the tests: .. code-block:: bash git clone git@github.com:django/django.git django-repo cd django-repo/tests + ./runtests.py + +.. versionchanged:: 1.7 + +Older versions of Django required running the tests like this:: + PYTHONPATH=..:$PYTHONPATH python ./runtests.py --settings=test_sqlite +``runtests.py`` now uses the Django package found at ``tests/../django`` (there +isn't a need to add this on your ``PYTHONPATH``) and ``test_sqlite`` for the +settings if settings aren't provided through either ``--settings`` or +:envvar:`DJANGO_SETTINGS_MODULE`. + .. _running-unit-tests-settings: Using another ``settings`` module diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 25d54d5269..f0ac66bce8 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -412,6 +412,8 @@ these changes. * ``django.utils.dictconfig`` will be removed. +* ``django.utils.importlib`` will be removed. + * ``django.utils.unittest`` will be removed. * The ``syncdb`` command will be removed. @@ -425,6 +427,18 @@ these changes. * ``allow_syncdb`` on database routers will no longer automatically become ``allow_migrate``. +* If models are organized in a package, Django will no longer look for + :ref:`initial SQL data<initial-sql>` in ``myapp/models/sql/``. Move your + custom SQL files to ``myapp/sql/``. + +* FastCGI support via the ``runfcgi`` management command will be + removed. Please deploy your project using WSGI. + +* ``django.utils.datastructures.SortedDict`` will be removed. Use + :class:`collections.OrderedDict` from the Python standard library instead. + +* ``ModelAdmin.declared_fieldsets`` will be removed. + 2.0 --- diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index dd3e86d8ae..c5c5f8f288 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -352,12 +352,6 @@ representation of the output. You can improve that by giving that method (in :file:`polls/models.py`) a few attributes, as follows:: - import datetime - from django.utils import timezone - from django.db import models - - from polls.models import Poll - class Poll(models.Model): # ... def was_published_recently(self): diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index f0c39e2c93..1165fbf18a 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -454,51 +454,6 @@ just as :func:`~django.shortcuts.get_object_or_404` -- except using :meth:`~django.db.models.query.QuerySet.get`. It raises :exc:`~django.http.Http404` if the list is empty. -Write a 404 (page not found) view -================================= - -When you raise :exc:`~django.http.Http404` from within a view, Django -will load a special view devoted to handling 404 errors. It finds it -by looking for the variable ``handler404`` in your root URLconf (and -only in your root URLconf; setting ``handler404`` anywhere else will -have no effect), which is a string in Python dotted syntax -- the same -format the normal URLconf callbacks use. A 404 view itself has nothing -special: It's just a normal view. - -You normally won't have to bother with writing 404 views. If you don't set -``handler404``, the built-in view :func:`django.views.defaults.page_not_found` -is used by default. Optionally, you can create a ``404.html`` template -in the root of your template directory. The default 404 view will then use that -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 - 404 view will never be used (and thus the ``404.html`` template will never - be rendered) because the traceback will be displayed instead. - -* The 404 view is also called if Django doesn't find a match after checking - every regular expression in the URLconf. - -Write a 500 (server error) view -=============================== - -Similarly, your root URLconf may define a ``handler500``, which points -to a view to call in case of server errors. Server errors happen when -you have runtime errors in view code. - -Likewise, you should create a ``500.html`` template at the root of your -template directory and add some content like "Something went wrong". - Use the template system ======================= diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index 39c3785f7c..63533d47ff 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -132,7 +132,7 @@ We identify a bug Fortunately, there's a little bug in the ``polls`` application for us to fix right away: the ``Poll.was_published_recently()`` method returns ``True`` if the ``Poll`` was published within the last day (which is correct) but also if -the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't). +the ``Poll``’s ``pub_date`` field is in the future (which certainly isn't). You can see this in the Admin; create a poll whose date lies in the future; you'll see that the ``Poll`` change list claims it was published recently. diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index a677bc9efd..638d219afe 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -66,6 +66,11 @@ different needs: where you'll turn to find the details of a particular function or whathaveyou. +* If you are interested in deploying a project for public use, our docs have + :doc:`several guides</howto/deployment/index>` for various deployment + setups as well as a :doc:`deployment checklist</howto/deployment/checklist>` + for some things you'll need to think about. + * Finally, there's some "specialized" documentation not usually relevant to most developers. This includes the :doc:`release notes </releases/index>` and :doc:`internals documentation </internals/index>` for those who want to add diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 4ad017c342..24058311a4 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -222,6 +222,8 @@ RedirectView .. attribute:: pattern_name + .. versionadded:: 1.6 + 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. diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 1a1a4d531b..1162b2a194 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -225,7 +225,7 @@ DateMixin .. attribute:: date_field The name of the ``DateField`` or ``DateTimeField`` in the - ``QuerySet``'s model that the date-based archive should use to + ``QuerySet``’s model that the date-based archive should use to determine the list of objects to display on the page. When :doc:`time zone support </topics/i18n/timezones>` is enabled and diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index b28bd11a71..67ca5429fa 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -63,6 +63,14 @@ MultipleObjectMixin A ``QuerySet`` that represents the objects. If provided, the value of ``queryset`` supersedes the value provided for :attr:`model`. + .. warning:: + + ``queryset`` is a class attribute with a *mutable* value so care + must be taken when using it directly. Before using it, either call + its :meth:`~django.db.models.query.QuerySet.all` method or + retrieve it with :meth:`get_queryset` which takes care of the + cloning behind the scenes. + .. attribute:: paginate_by An integer specifying how many objects should be displayed per page. If diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index bbe930d79e..1fce24acc5 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -23,6 +23,14 @@ SingleObjectMixin A ``QuerySet`` that represents the objects. If provided, the value of ``queryset`` supersedes the value provided for :attr:`model`. + .. warning:: + + ``queryset`` is a class attribute with a *mutable* value so care + must be taken when using it directly. Before using it, either call + its :meth:`~django.db.models.query.QuerySet.all` method or + retrieve it with :meth:`get_queryset` which takes care of the + cloning behind the scenes. + .. attribute:: slug_field The name of the field on the model that contains the slug. By default, diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 65d096921f..58d8244525 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -271,7 +271,7 @@ Making actions available site-wide This makes the `export_selected_objects` action globally available as an action named `"export_selected_objects"`. You can explicitly give the action - a name -- good if you later want to programatically :ref:`remove the action + a name -- good if you later want to programmatically :ref:`remove the action <disabling-admin-actions>` -- by passing a second argument to :meth:`AdminSite.add_action()`:: diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index e5e9428805..8feb574efd 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1218,6 +1218,14 @@ templates used by the :class:`ModelAdmin` views: changelist that will be linked to the change view, as described in the :attr:`ModelAdmin.list_display_links` section. +.. method:: ModelAdmin.get_fields(self, request, obj=None) + + .. versionadded:: 1.7 + + The ``get_fields`` method is given the ``HttpRequest`` and the ``obj`` + being edited (or ``None`` on an add form) and is expected to return a list + of fields, as described above in the :attr:`ModelAdmin.fields` section. + .. method:: ModelAdmin.get_fieldsets(self, request, obj=None) The ``get_fieldsets`` method is given the ``HttpRequest`` and the ``obj`` @@ -1233,6 +1241,14 @@ templates used by the :class:`ModelAdmin` views: to return the same kind of sequence type as for the :attr:`~ModelAdmin.list_filter` attribute. +.. method:: ModelAdmin.get_search_fields(self, request) + + .. versionadded:: 1.7 + + The ``get_search_fields`` method is given the ``HttpRequest`` and is expected + to return the same kind of sequence type as for the + :attr:`~ModelAdmin.search_fields` attribute. + .. method:: ModelAdmin.get_inline_instances(self, request, obj=None) .. versionadded:: 1.5 @@ -1389,6 +1405,15 @@ templates used by the :class:`ModelAdmin` views: kwargs['choices'] += (('ready', 'Ready for deployment'),) return super(MyModelAdmin, self).formfield_for_choice_field(db_field, request, **kwargs) + .. admonition:: Note + + Any ``choices`` attribute set on the formfield will limited to the form + field only. If the corresponding field on the model has choices set, + the choices provided to the form must be a valid subset of those + choices, otherwise the form submission will fail with + a :exc:`~django.core.exceptions.ValidationError` when the model itself + is validated before saving. + .. method:: ModelAdmin.get_changelist(self, request, **kwargs) Returns the ``Changelist`` class to be used for listing. By default, @@ -1590,7 +1615,7 @@ in your own admin JavaScript without including a second copy, you can use the The embedded jQuery has been upgraded from 1.4.2 to 1.9.1. The :class:`ModelAdmin` class requires jQuery by default, so there is no need -to add jQuery to your ``ModelAdmin``'s list of media resources unless you have +to add jQuery to your ``ModelAdmin``’s list of media resources unless you have a specifc need. For example, if you require the jQuery library to be in the global namespace (for example when using third-party jQuery plugins) or if you need a newer version of jQuery, you will have to include your own copy. @@ -2162,6 +2187,10 @@ Templates can override or extend base admin templates as described in Path to a custom template that will be used by the admin site main index view. +.. attribute:: AdminSite.app_index_template + + Path to a custom template that will be used by the admin site app index view. + .. attribute:: AdminSite.login_template Path to a custom template that will be used by the admin site login view. diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index dfda8add4b..4fe87e9872 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -114,11 +114,13 @@ Methods Always returns ``True`` (as opposed to ``AnonymousUser.is_authenticated()`` which always returns ``False``). This is a way to tell if the user has been authenticated. This does not - imply any permissions, and doesn't check if the user is active - it - only indicates that ``request.user`` has been populated by the - :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` with - a :class:`~django.contrib.auth.models.User` object representing the - currently logged-in user. + imply any permissions, and doesn't check if the user is active or has + a valid session. Even though normally you will call this method on + ``request.user`` to find out whether it has been populated by the + :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` + (representing the currently logged-in user), you should know this method + returns ``True`` for any :class:`~django.contrib.auth.models.User` + instance. .. method:: get_full_name() @@ -243,7 +245,7 @@ Manager methods be called. The ``extra_fields`` keyword arguments are passed through to the - :class:`~django.contrib.auth.models.User`'s ``__init__`` method to + :class:`~django.contrib.auth.models.User`’s ``__init__`` method to allow setting arbitrary fields on a :ref:`custom User model <auth-custom-user>`. diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index 796e257200..5f0badfadb 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -54,7 +54,7 @@ following model, which would represent entries in a Weblog:: Now, suppose that we want the following steps to be applied whenever a new comment is posted on an ``Entry``: -1. If the ``Entry``'s ``enable_comments`` field is ``False``, the +1. If the ``Entry``’s ``enable_comments`` field is ``False``, the comment will simply be disallowed (i.e., immediately deleted). 2. If the ``enable_comments`` field is ``True``, the comment will be diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 4467ed3a6e..7b53fc8c12 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -178,6 +178,15 @@ Sitemap class reference representing the last-modified date/time for *every* object returned by :attr:`~Sitemap.items()`. + .. versionadded:: 1.7 + + If all items in a sitemap have a :attr:`~Sitemap.lastmod`, the sitemap + generated by :func:`views.sitemap` will have a ``Last-Modified`` + header equal to the latest ``lastmod``. You can activate the + :class:`~django.middleware.http.ConditionalGetMiddleware` to make + Django respond appropriately to requests with an ``If-Modified-Since`` + header which will prevent sending the sitemap if it hasn't changed. + .. attribute:: Sitemap.changefreq **Optional.** Either a method or attribute. diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 806d135deb..f96c2e8a1b 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -226,7 +226,7 @@ CachedStaticFilesStorage would be replaced by calling the :meth:`~django.core.files.storage.Storage.url` - method of the ``CachedStaticFilesStorage`` storage backend, ultimatively + method of the ``CachedStaticFilesStorage`` storage backend, ultimately saving a ``'css/styles.55e7cbb9ba48.css'`` file with the following content: @@ -350,6 +350,12 @@ This view function serves static files in development. **insecure**. This is only intended for local development, and should **never be used in production**. +.. versionchanged:: 1.7 + + This view will now raise an :exc:`~django.http.Http404` exception instead + of :exc:`~django.core.exceptions.ImproperlyConfigured` when + :setting:`DEBUG` is ``False``. + .. note:: To guess the served files' content types, this view relies on the diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 9a0fb5830e..996950ef56 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -49,17 +49,17 @@ are views which can be used in your :doc:`URLconf </topics/http/urls>`. A simple example ---------------- -This simple example, taken from `chicagocrime.org`_, describes a feed of the -latest five news items:: +This simple example, taken from a hypothetical police beat news site describes +a feed of the latest five news items:: from django.contrib.syndication.views import Feed from django.core.urlresolvers import reverse - from chicagocrime.models import NewsItem + from policebeat.models import NewsItem class LatestEntriesFeed(Feed): - title = "Chicagocrime.org site news" + title = "Police beat site news" link = "/sitenews/" - description = "Updates on changes and additions to chicagocrime.org." + description = "Updates on changes and additions to police beat central." def items(self): return NewsItem.objects.order_by('-pub_date')[:5] @@ -199,22 +199,20 @@ into those elements. are responsible for doing all necessary URL quoting and conversion to ASCII inside the method itself. -.. _chicagocrime.org: http://www.chicagocrime.org/ - A complex example ----------------- The framework also supports more complex feeds, via arguments. -For example, `chicagocrime.org`_ offers an RSS feed of recent crimes for every -police beat in Chicago. It'd be silly to create a separate +For example, a website could offer an RSS feed of recent crimes for every +police beat in a city. It'd be silly to create a separate :class:`~django.contrib.syndication.views.Feed` class for each police beat; that would violate the :ref:`DRY principle <dry>` and would couple data to programming logic. Instead, the syndication framework lets you access the arguments passed from your :doc:`URLconf </topics/http/urls>` so feeds can output items based on information in the feed's URL. -On chicagocrime.org, the police-beat feeds are accessible via URLs like this: +The police beat feeds could be accessible via URLs like this: * :file:`/beats/613/rss/` -- Returns recent crimes for beat 613. * :file:`/beats/1424/rss/` -- Returns recent crimes for beat 1424. @@ -238,7 +236,7 @@ Here's the code for these beat-specific feeds:: return get_object_or_404(Beat, pk=beat_id) def title(self, obj): - return "Chicagocrime.org: Crimes for beat %s" % obj.beat + return "Police beat central: Crimes for beat %s" % obj.beat def link(self, obj): return obj.get_absolute_url() @@ -339,13 +337,13 @@ URLconf to add the extra versions. Here's a full example:: from django.contrib.syndication.views import Feed - from chicagocrime.models import NewsItem + from policebeat.models import NewsItem from django.utils.feedgenerator import Atom1Feed class RssSiteNewsFeed(Feed): - title = "Chicagocrime.org site news" + title = "Police beat site news" link = "/sitenews/" - description = "Updates on changes and additions to chicagocrime.org." + description = "Updates on changes and additions to police beat central." def items(self): return NewsItem.objects.order_by('-pub_date')[:5] diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 05fc8a1191..ab83e2cf33 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -233,7 +233,7 @@ probably be using this flag. .. django-admin-option:: --pks By default, ``dumpdata`` will output all the records of the model, but -you can use the ``--pks`` option to specify a comma seperated list of +you can use the ``--pks`` option to specify a comma separated list of primary keys on which to filter. This is only available when dumping one model. @@ -251,8 +251,8 @@ prompts. The :djadminopt:`--database` option may be used to specify the database to flush. ---no-initial-data -~~~~~~~~~~~~~~~~~ +``--no-initial-data`` +~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 1.5 @@ -558,7 +558,7 @@ several lines in language files. .. django-admin-option:: --no-location -Use the ``--no-location`` option to not write '``#: filename:line``' +Use the ``--no-location`` option to not write '``#: filename:line``’ comment lines in language files. Note that using this option makes it harder for technically skilled translators to understand each message's context. @@ -617,6 +617,9 @@ runfcgi [options] .. django-admin:: runfcgi +.. deprecated:: 1.7 + FastCGI support is deprecated and will be removed in Django 1.9. + Starts a set of FastCGI processes suitable for use with any Web server that supports the FastCGI protocol. See the :doc:`FastCGI deployment documentation </howto/deployment/fastcgi>` for details. Requires the Python FastCGI module from @@ -780,7 +783,7 @@ You can provide an IPv6 address surrounded by brackets A hostname containing ASCII-only characters can also be used. If the :doc:`staticfiles</ref/contrib/staticfiles>` contrib app is enabled -(default in new projects) the :djadmin:`runserver` command will be overriden +(default in new projects) the :djadmin:`runserver` command will be overridden with its own :ref:`runserver<staticfiles-runserver>` command. .. django-admin-option:: --noreload @@ -1432,6 +1435,19 @@ that ``django-admin.py`` should print to the console. * ``2`` means verbose output. * ``3`` means *very* verbose output. +.. django-admin-option:: --no-color + +.. versionadded:: 1.7 + +Example usage:: + + django-admin.py sqlall --no-color + +By default, ``django-admin.py`` will format the output to be colorized. For +example, errors will be printed to the console in red and SQL statements will +be syntax highlighted. To prevent this and have a plain text output, pass the +``--no-color`` option when running your command. + Common options ============== diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index b15bbea8fa..bee8559ab0 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -6,24 +6,29 @@ Django Exceptions Django raises some Django specific exceptions as well as many standard Python exceptions. -Django-specific Exceptions -========================== +Django Core Exceptions +====================== .. module:: django.core.exceptions - :synopsis: Django specific exceptions + :synopsis: Django core exceptions + +Django core exception classes are defined in :mod:`django.core.exceptions`. ObjectDoesNotExist and DoesNotExist ----------------------------------- .. exception:: DoesNotExist -.. exception:: ObjectDoesNotExist - The :exc:`DoesNotExist` exception is raised when an object is not found - for the given parameters of a query. + The ``DoesNotExist`` exception is raised when an object is not found for + the given parameters of a query. Django provides a ``DoesNotExist`` + exception as an attribute of each model class to identify the class of + object that could not be found and to allow you to catch a particular model + class with ``try/except``. + +.. exception:: ObjectDoesNotExist - :exc:`ObjectDoesNotExist` is defined in :mod:`django.core.exceptions`. - :exc:`DoesNotExist` is a subclass of the base :exc:`ObjectDoesNotExist` - exception that is provided on every model class as a way of - identifying the specific type of object that could not be found. + The base class for ``DoesNotExist`` exceptions; a ``try/except`` for + ``ObjectDoesNotExist`` will catch ``DoesNotExist`` exceptions for all + models. See :meth:`~django.db.models.query.QuerySet.get()` for further information on :exc:`ObjectDoesNotExist` and :exc:`DoesNotExist`. @@ -121,6 +126,11 @@ ValidationError .. currentmodule:: django.core.urlresolvers +URL Resolver exceptions +======================= + +URL Resolver exceptions are defined in :mod:`django.core.urlresolvers`. + NoReverseMatch -------------- .. exception:: NoReverseMatch @@ -134,9 +144,10 @@ NoReverseMatch Database Exceptions =================== +Database exceptions are provided in :mod:`django.db`. + Django wraps the standard database exceptions so that your Django code has a -guaranteed common implementation of these classes. These database exceptions -are provided in :mod:`django.db`. +guaranteed common implementation of these classes. .. exception:: Error .. exception:: InterfaceError @@ -160,34 +171,37 @@ to Python 3.) .. versionchanged:: 1.6 - Previous version of Django only wrapped ``DatabaseError`` and + Previous versions of Django only wrapped ``DatabaseError`` and ``IntegrityError``, and did not provide ``__cause__``. .. exception:: models.ProtectedError Raised to prevent deletion of referenced objects when using -:attr:`django.db.models.PROTECT`. Subclass of :exc:`IntegrityError`. +:attr:`django.db.models.PROTECT`. :exc:`models.ProtectedError` is a subclass +of :exc:`IntegrityError`. .. currentmodule:: django.http Http Exceptions =============== +Http exceptions are provided in :mod:`django.http`. + .. exception:: UnreadablePostError The :exc:`UnreadablePostError` is raised when a user cancels an upload. - It is available from :mod:`django.http`. .. currentmodule:: django.db.transaction Transaction Exceptions ====================== +Transaction exceptions are defined in :mod:`django.db.transaction`. + .. exception:: TransactionManagementError The :exc:`TransactionManagementError` is raised for any and all problems - related to database transactions. It is available from - :mod:`django.db.transaction`. + related to database transactions. Python Exceptions ================= diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 7c1601d3ea..780cb5d4f7 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -527,6 +527,11 @@ Note that the label suffix is added only if the last character of the label isn't a punctuation character (in English, those are ``.``, ``!``, ``?`` or ``:``). +.. versionadded:: 1.6 + +You can also customize the ``label_suffix`` on a per-field basis using the +``label_suffix`` parameter to :meth:`~django.forms.BoundField.label_tag`. + Notes on field ordering ~~~~~~~~~~~~~~~~~~~~~~~ @@ -653,7 +658,7 @@ when printed:: >>> str(f['subject'].errors) '' -.. method:: BoundField.label_tag(contents=None, attrs=None) +.. method:: BoundField.label_tag(contents=None, attrs=None, label_suffix=None) To separately render the label tag of a form field, you can call its ``label_tag`` method:: @@ -671,6 +676,14 @@ additional attributes for the ``<label>`` tag. The label now includes the form's :attr:`~django.forms.Form.label_suffix` (a colon, by default). +.. versionadded:: 1.6 + + The optional ``label_suffix`` parameter allows you to override the form's + :attr:`~django.forms.Form.label_suffix`. For example, you can use an empty + string to hide the label on selected fields. If you need to do this in a + template, you could write a custom filter to allow passing parameters to + ``label_tag``. + .. method:: BoundField.css_classes() When you use Django's rendering shortcuts, CSS classes are used to diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index ef4ed729bd..e7c6612a72 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -877,7 +877,7 @@ Slightly complex built-in ``Field`` classes * Normalizes to: the type returned by the ``compress`` method of the subclass. * Validates that the given value against each of the fields specified as an argument to the ``MultiValueField``. - * Error message keys: ``required``, ``invalid`` + * Error message keys: ``required``, ``invalid``, ``incomplete`` Aggregates the logic of multiple fields that together produce a single value. @@ -898,6 +898,45 @@ Slightly complex built-in ``Field`` classes Once all fields are cleaned, the list of clean values is combined into a single value by :meth:`~MultiValueField.compress`. + Also takes one extra optional argument: + + .. attribute:: require_all_fields + + .. versionadded:: 1.7 + + Defaults to ``True``, in which case a ``required`` validation error + will be raised if no value is supplied for any field. + + When set to ``False``, the :attr:`Field.required` attribute can be set + to ``False`` for individual fields to make them optional. If no value + is supplied for a required field, an ``incomplete`` validation error + will be raised. + + A default ``incomplete`` error message can be defined on the + :class:`MultiValueField` subclass, or different messages can be defined + on each individual field. For example:: + + from django.core.validators import RegexValidator + + class PhoneField(MultiValueField): + def __init__(self, *args, **kwargs): + # Define one message for all fields. + error_messages = { + 'incomplete': 'Enter a country code and phone number.', + } + # Or define a different message for each field. + fields = ( + CharField(error_messages={'incomplete': 'Enter a country code.'}, + validators=[RegexValidator(r'^\d+$', 'Enter a valid country code.')]), + CharField(error_messages={'incomplete': 'Enter a phone number.'}, + validators=[RegexValidator(r'^\d+$', 'Enter a valid phone number.')]), + CharField(validators=[RegexValidator(r'^\d+$', 'Enter a valid extension.')], + required=False), + ) + super(PhoneField, self).__init__( + self, error_messages=error_messages, fields=fields, + require_all_fields=False, *args, **kwargs) + .. attribute:: MultiValueField.widget Must be a subclass of :class:`django.forms.MultiWidget`. diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 8ab1c26831..255b665c42 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -75,10 +75,8 @@ overridden: any validation that requires access to multiple fields from the form at once. This is where you might put in things to check that if field ``A`` is supplied, field ``B`` must contain a valid email address and the - like. The data that this method returns is the final ``cleaned_data`` - attribute for the form, so don't forget to return the full list of - cleaned data if you override this method (by default, ``Form.clean()`` - just returns ``self.cleaned_data``). + like. This method can return a completely different dictionary if it wishes, + which will be used as the ``cleaned_data``. Note that any errors raised by your ``Form.clean()`` override will not be associated with any field in particular. They go into a special @@ -171,7 +169,7 @@ following guidelines: Putting it all together:: - raise ValidationErrror( + raise ValidationError( _('Invalid value: %(value)s'), code='invalid', params={'value': '42'}, @@ -365,6 +363,8 @@ write a cleaning method that operates on the ``recipients`` field, like so:: Cleaning and validating fields that depend on each other ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.forms.Form.clean + Suppose we add another requirement to our contact form: if the ``cc_myself`` field is ``True``, the ``subject`` must contain the word ``"help"``. We are performing validation on more than one field at a time, so the form's @@ -403,9 +403,6 @@ example:: raise forms.ValidationError("Did not send for 'help' in " "the subject despite CC'ing yourself.") - # Always return the full collection of cleaned data. - return cleaned_data - In this code, if the validation error is raised, the form will display an error message at the top of the form (normally) describing the problem. @@ -443,9 +440,6 @@ sample) looks like this:: del cleaned_data["cc_myself"] del cleaned_data["subject"] - # Always return the full collection of cleaned data. - return cleaned_data - As you can see, this approach requires a bit more effort, not withstanding the extra design effort to create a sensible form display. The details are worth noting, however. Firstly, earlier we mentioned that you might need to check if diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 5b66776cfc..080d1fea86 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -52,8 +52,7 @@ widget on the field. In the following example, the :attr:`~django.forms.extras.widgets.SelectDateWidget.years` attribute is set for a :class:`~django.forms.extras.widgets.SelectDateWidget`:: - from django.forms.fields import DateField, ChoiceField, MultipleChoiceField - from django.forms.widgets import RadioSelect, CheckboxSelectMultiple + from django import forms from django.forms.extras.widgets import SelectDateWidget BIRTH_YEAR_CHOICES = ('1980', '1981', '1982') @@ -62,9 +61,9 @@ for a :class:`~django.forms.extras.widgets.SelectDateWidget`:: ('black', 'Black')) class SimpleForm(forms.Form): - birth_year = DateField(widget=SelectDateWidget(years=BIRTH_YEAR_CHOICES)) + birth_year = forms.DateField(widget=SelectDateWidget(years=BIRTH_YEAR_CHOICES)) favorite_colors = forms.MultipleChoiceField(required=False, - widget=CheckboxSelectMultiple, choices=FAVORITE_COLORS_CHOICES) + widget=forms.CheckboxSelectMultiple, choices=FAVORITE_COLORS_CHOICES) See the :ref:`built-in widgets` for more information about which widgets are available and which arguments they accept. diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 4898bab636..d011f054ac 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -37,7 +37,7 @@ defines. See the :doc:`cache documentation </topics/cache>`. Adds a few conveniences for perfectionists: * Forbids access to user agents in the :setting:`DISALLOWED_USER_AGENTS` - setting, which should be a list of strings. + setting, which should be a list of compiled regular expression objects. * Performs URL rewriting based on the :setting:`APPEND_SLASH` and :setting:`PREPEND_WWW` settings. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 869996643f..01215884c4 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -152,11 +152,20 @@ method to retrieve the human-readable name for the field's current value. See :meth:`~django.db.models.Model.get_FOO_display` in the database API documentation. -Finally, note that choices can be any iterable object -- not necessarily a list -or tuple. This lets you construct choices dynamically. But if you find yourself -hacking :attr:`~Field.choices` to be dynamic, you're probably better off using a -proper database table with a :class:`ForeignKey`. :attr:`~Field.choices` is -meant for static data that doesn't change much, if ever. +Note that choices can be any iterable object -- not necessarily a list or tuple. +This lets you construct choices dynamically. But if you find yourself hacking +:attr:`~Field.choices` to be dynamic, you're probably better off using a proper +database table with a :class:`ForeignKey`. :attr:`~Field.choices` is meant for +static data that doesn't change much, if ever. + +.. versionadded:: 1.7 + +Unless :attr:`blank=False<Field.blank>` is set on the field along with a +:attr:`~Field.default` then a label containing ``"---------"`` will be rendered +with the select box. To override this behavior, add a tuple to ``choices`` +containing ``None``; e.g. ``(None, 'Your String For Display')``. +Alternatively, you can use an empty string instead of ``None`` where this makes +sense - such as on a :class:`~django.db.models.CharField`. ``db_column`` ------------- @@ -874,6 +883,9 @@ are converted to lowercase. ``192.0.2.1``. Default is disabled. Can only be used when ``protocol`` is set to ``'both'``. +If you allow for blank values, you have to allow for null values since blank +values are stored as null. + ``NullBooleanField`` -------------------- diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index f06866d9a1..cb8570afdc 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -118,7 +118,7 @@ validation for your own manually created models. For example:: article.full_clean() except ValidationError as e: # Do something based on the errors contained in e.message_dict. - # Display them to a user, or handle them programatically. + # Display them to a user, or handle them programmatically. pass The first step ``full_clean()`` performs is to clean each individual field. @@ -140,15 +140,19 @@ attributes on your model if desired. For instance, you could use it to automatically provide a value for a field, or to do validation that requires access to more than a single field:: - def clean(self): - import datetime - from django.core.exceptions import ValidationError - # Don't allow draft entries to have a pub_date. - if self.status == 'draft' and self.pub_date is not None: - raise ValidationError('Draft entries may not have a publication date.') - # Set the pub_date for published items if it hasn't been set already. - if self.status == 'published' and self.pub_date is None: - self.pub_date = datetime.date.today() + import datetime + from django.core.exceptions import ValidationError + from django.db import models + + class Article(models.Model): + ... + def clean(self): + # Don't allow draft entries to have a pub_date. + if self.status == 'draft' and self.pub_date is not None: + raise ValidationError('Draft entries may not have a publication date.') + # Set the pub_date for published items if it hasn't been set already. + if self.status == 'published' and self.pub_date is None: + self.pub_date = datetime.date.today() Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by ``Model.clean()`` will be stored in a special key error dictionary key, diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 6eeed51b6f..6415a9d2da 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -90,7 +90,7 @@ Django quotes column and table names behind the scenes. The name of an orderable field in the model, typically a :class:`DateField`, :class:`DateTimeField`, or :class:`IntegerField`. This specifies the default - field to use in your model :class:`Manager`'s + field to use in your model :class:`Manager`’s :meth:`~django.db.models.query.QuerySet.latest` and :meth:`~django.db.models.query.QuerySet.earliest` methods. @@ -227,9 +227,8 @@ Django quotes column and table names behind the scenes. .. attribute:: Options.permissions Extra permissions to enter into the permissions table when creating this object. - Add, delete and change permissions are automatically created for each object - that has ``admin`` set. This example specifies an extra permission, - ``can_deliver_pizzas``:: + Add, delete and change permissions are automatically created for each + model. This example specifies an extra permission, ``can_deliver_pizzas``:: permissions = (("can_deliver_pizzas", "Can deliver pizzas"),) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 3963785733..0f08022179 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -110,7 +110,7 @@ described here. .. admonition:: You can't share pickles between versions - Pickles of QuerySets are only valid for the version of Django that + Pickles of ``QuerySets`` are only valid for the version of Django that was used to generate them. If you generate a pickle using Django version N, there is no guarantee that pickle will be readable with Django version N+1. Pickles should not be used as part of a long-term @@ -121,9 +121,7 @@ described here. QuerySet API ============ -Though you usually won't create one manually — you'll go through a -:class:`~django.db.models.Manager` — here's the formal declaration of a -``QuerySet``: +Here's the formal declaration of a ``QuerySet``: .. class:: QuerySet([model=None, query=None, using=None]) @@ -300,14 +298,30 @@ Be cautious when ordering by fields in related models if you are also using :meth:`distinct()`. See the note in :meth:`distinct` for an explanation of how related model ordering can change the expected results. -It is permissible to specify a multi-valued field to order the results by (for -example, a :class:`~django.db.models.ManyToManyField` field). Normally -this won't be a sensible thing to do and it's really an advanced usage -feature. However, if you know that your queryset's filtering or available data -implies that there will only be one ordering piece of data for each of the main -items you are selecting, the ordering may well be exactly what you want to do. -Use ordering on multi-valued fields with care and make sure the results are -what you expect. +.. note:: + It is permissible to specify a multi-valued field to order the results by + (for example, a :class:`~django.db.models.ManyToManyField` field, or the + reverse relation of a :class:`~django.db.models.ForeignKey` field). + + Consider this case:: + + class Event(Model): + parent = models.ForeignKey('self', related_name='children') + date = models.DateField() + + Event.objects.order_by('children__date') + + Here, there could potentially be multiple ordering data for each ``Event``; + each ``Event`` with multiple ``children`` will be returned multiple times + into the new ``QuerySet`` that ``order_by()`` creates. In other words, + using ``order_by()`` on the ``QuerySet`` could return more items than you + were working on to begin with - which is probably neither expected nor + useful. + + Thus, take care when using multi-valued field to order the results. **If** + you can be sure that there will only be one ordering piece of data for each + of the items you're ordering, this approach should not present problems. If + not, make sure the results are what you expect. There's no way to specify whether ordering should be case sensitive. With respect to case-sensitivity, Django will order results however your database @@ -388,7 +402,7 @@ field names, the database will only compare the specified field names. .. note:: When you specify field names, you *must* provide an ``order_by()`` in the - QuerySet, and the fields in ``order_by()`` must start with the fields in + ``QuerySet``, and the fields in ``order_by()`` must start with the fields in ``distinct()``, in the same order. For example, ``SELECT DISTINCT ON (a)`` gives you the first row for each @@ -789,8 +803,8 @@ stop the deluge of database queries that is caused by accessing related objects, but the strategy is quite different. ``select_related`` works by creating a SQL join and including the fields of the -related object in the SELECT statement. For this reason, ``select_related`` gets -the related objects in the same database query. However, to avoid the much +related object in the ``SELECT`` statement. For this reason, ``select_related`` +gets the related objects in the same database query. However, to avoid the much larger result set that would result from joining across a 'many' relationship, ``select_related`` is limited to single-valued relationships - foreign key and one-to-one. @@ -819,39 +833,54 @@ For example, suppose you have these models:: return u"%s (%s)" % (self.name, u", ".join([topping.name for topping in self.toppings.all()])) -and run this code:: +and run:: >>> Pizza.objects.all() [u"Hawaiian (ham, pineapple)", u"Seafood (prawns, smoked salmon)"... -The problem with this code is that it will run a query on the Toppings table for -**every** item in the Pizza ``QuerySet``. Using ``prefetch_related``, this can -be reduced to two: +The problem with this is that every time ``Pizza.__unicode__()`` asks for +``self.toppings.all()`` it has to query the database, so +``Pizza.objects.all()`` will run a query on the Toppings table for **every** +item in the Pizza ``QuerySet``. + +We can reduce to just two queries using ``prefetch_related``: >>> Pizza.objects.all().prefetch_related('toppings') -All the relevant toppings will be fetched in a single query, and used to make -``QuerySets`` that have a pre-filled cache of the relevant results. These -``QuerySets`` are then used in the ``self.toppings.all()`` calls. +This implies a ``self.toppings.all()`` for each ``Pizza``; now each time +``self.toppings.all()`` is called, instead of having to go to the database for +the items, it will find them in a prefetched ``QuerySet`` cache that was +populated in a single query. -The additional queries are executed after the QuerySet has begun to be evaluated -and the primary query has been executed. Note that the result cache of the -primary QuerySet and all specified related objects will then be fully loaded -into memory, which is often avoided in other cases - even after a query has been -executed in the database, QuerySet normally tries to make uses of chunking -between the database to avoid loading all objects into memory before you need -them. +That is, all the relevant toppings will have been fetched in a single query, +and used to make ``QuerySets`` that have a pre-filled cache of the relevant +results; these ``QuerySets`` are then used in the ``self.toppings.all()`` calls. -Also remember that, as always with QuerySets, any subsequent chained methods -which imply a different database query will ignore previously cached results, -and retrieve data using a fresh database query. So, if you write the following: +The additional queries in ``prefetch_related()`` are executed after the +``QuerySet`` has begun to be evaluated and the primary query has been executed. - >>> pizzas = Pizza.objects.prefetch_related('toppings') - >>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas] +Note that the result cache of the primary ``QuerySet`` and all specified related +objects will then be fully loaded into memory. This changes the typical +behavior of ``QuerySets``, which normally try to avoid loading all objects into +memory before they are needed, even after a query has been executed in the +database. -...then the fact that ``pizza.toppings.all()`` has been prefetched will not help -you - in fact it hurts performance, since you have done a database query that -you haven't used. So use this feature with caution! +.. note:: + + Remember that, as always with ``QuerySets``, any subsequent chained methods + which imply a different database query will ignore previously cached + results, and retrieve data using a fresh database query. So, if you write + the following: + + >>> pizzas = Pizza.objects.prefetch_related('toppings') + >>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas] + + ...then the fact that ``pizza.toppings.all()`` has been prefetched will not + help you. The ``prefetch_related('toppings')`` implied + ``pizza.toppings.all()``, but ``pizza.toppings.filter()`` is a new and + different query. The prefetched cache can't help here; in fact it hurts + performance, since you have done a database query that you haven't used. So + use this feature with caution! You can also use the normal join syntax to do related fields of related fields. Suppose we have an additional model to the example above:: @@ -904,7 +933,7 @@ additional queries on the ``ContentType`` table if the relevant rows have not already been fetched. ``prefetch_related`` in most cases will be implemented using a SQL query that -uses the 'IN' operator. This means that for a large QuerySet a large 'IN' clause +uses the 'IN' operator. This means that for a large ``QuerySet`` a large 'IN' clause could be generated, which, depending on the database, might have performance problems of its own when it comes to parsing or executing the SQL query. Always profile for your use case! @@ -979,14 +1008,13 @@ of the arguments is required, but you should use at least one of them. ``select_params`` parameter. Since ``select_params`` is a sequence and the ``select`` attribute is a dictionary, some care is required so that the parameters are matched up correctly with the extra select pieces. - In this situation, you should use a - :class:`django.utils.datastructures.SortedDict` for the ``select`` - value, not just a normal Python dictionary. + In this situation, you should use a :class:`collections.OrderedDict` for + the ``select`` value, not just a normal Python dictionary. This will work, for example:: Blog.objects.extra( - select=SortedDict([('a', '%s'), ('b', '%s')]), + select=OrderedDict([('a', '%s'), ('b', '%s')]), select_params=('one', 'two')) The only thing to be careful about when using select parameters in @@ -1264,6 +1292,28 @@ unexpectedly blocking. Using ``select_for_update`` on backends which do not support ``SELECT ... FOR UPDATE`` (such as SQLite) will have no effect. +raw +~~~ + +.. method:: raw(raw_query, params=None, translations=None) + +.. versionchanged:: 1.7 + + ``raw`` was moved to the ``QuerySet`` class. It was previously only on + :class:`~django.db.models.Manager`. + +Takes a raw SQL query, executes it, and returns a +``django.db.models.query.RawQuerySet`` instance. This ``RawQuerySet`` instance +can be iterated over just like an normal ``QuerySet`` to provide object instances. + +See the :ref:`executing-raw-queries` for more information. + +.. warning:: + + ``raw()`` always triggers a new query and doesn't account for previous + filtering. As such, it should generally be called from the ``Manager`` or + from a fresh ``QuerySet`` instance. + Methods that do not return QuerySets ------------------------------------ @@ -1866,6 +1916,17 @@ DO_NOTHING do not prevent taking the fast-path in deletion. Note that the queries generated in object deletion is an implementation detail subject to change. +as_manager +~~~~~~~~~~ + +.. classmethod:: as_manager() + +.. versionadded:: 1.7 + +Class method that returns an instance of :class:`~django.db.models.Manager` +with a copy of the ``QuerySet``’s methods. See +:ref:`create-manager-with-queryset-methods` for more details. + .. _field-lookups: Field lookups diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 060ec02e91..c57a1470d6 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -495,6 +495,26 @@ In addition, ``QueryDict`` has the following methods: >>> q.lists() [(u'a', [u'1', u'2', u'3'])] +.. method:: QueryDict.pop(key) + + Returns a list of values for the given key and removes them from the + dictionary. Raises ``KeyError`` if the key does not exist. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.pop('a') + [u'1', u'2', u'3'] + +.. method:: QueryDict.popitem() + + Removes an arbitrary member of the dictionary (since there's no concept + of ordering), and returns a two value tuple containing the key and a list + of all values for the key. Raises ``KeyError`` when called on an empty + dictionary. For example:: + + >>> q = QueryDict('a=1&a=2&a=3', mutable=True) + >>> q.popitem() + (u'a', [u'1', u'2', u'3']) + .. method:: QueryDict.dict() Returns ``dict`` representation of ``QueryDict``. For every (key, list) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index c970311342..38d7275aed 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1744,7 +1744,7 @@ Default:: A tuple of template loader classes, specified as strings. Each ``Loader`` class knows how to import templates from a particular source. Optionally, a tuple can be -used instead of a string. The first item in the tuple should be the ``Loader``'s +used instead of a string. The first item in the tuple should be the ``Loader``’s module, subsequent items are passed to the ``Loader`` during initialization. See :doc:`/ref/templates/api`. @@ -2478,7 +2478,7 @@ files</howto/static-files/index>` for more details about usage. your static files from their permanent locations into one directory for ease of deployment; it is **not** a place to store your static files permanently. You should do that in directories that will be found by - :doc:`staticfiles</ref/contrib/staticfiles>`'s + :doc:`staticfiles</ref/contrib/staticfiles>`’s :setting:`finders<STATICFILES_FINDERS>`, which by default, are ``'static/'`` app sub-directories and any directories you include in :setting:`STATICFILES_DIRS`). diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index f7dd0121d1..87822fe7f8 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -708,7 +708,7 @@ class. Here are the template loaders that come with Django: with your own ``admin/base_site.html`` in ``myproject.polls``. You must then make sure that your ``myproject.polls`` comes *before* ``django.contrib.admin`` in :setting:`INSTALLED_APPS`, otherwise - ``django.contrib.admin``'s will be loaded first and yours will be ignored. + ``django.contrib.admin``’s will be loaded first and yours will be ignored. Note that the loader performs an optimization when it is first imported: it caches a list of which :setting:`INSTALLED_APPS` packages have a diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 6b614e0c83..d31de35006 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -105,6 +105,10 @@ to distinguish caches by the ``Accept-language`` header. .. class:: SortedDict +.. deprecated:: 1.7 + ``SortedDict`` is deprecated and will be removed in Django 1.9. Use + :class:`collections.OrderedDict` instead. + The :class:`django.utils.datastructures.SortedDict` class is a dictionary that keeps its keys in the order in which they're inserted. @@ -427,6 +431,64 @@ Atom1Feed .. module:: django.utils.functional :synopsis: Functional programming tools. +.. class:: cached_property(object) + + The ``@cached_property`` decorator caches the result of a method with a + single ``self`` argument as a property. The cached result will persist + as long as the instance does, so if the instance is passed around and the + function subsequently invoked, the cached result will be returned. + + Consider a typical case, where a view might need to call a model's method + to perform some computation, before placing the model instance into the + context, where the template might invoke the method once more:: + + # the model + class Person(models.Model): + + def friends(self): + # expensive computation + ... + return friends + + # in the view: + if person.friends(): + + # in the template: + {% for friend in person.friends %} + + Here, ``friends()`` will be called twice. Since the instance ``person`` in + the view and the template are the same, ``@cached_property`` can avoid + that:: + + from django.utils.functional import cached_property + + @cached_property + def friends(self): + # expensive computation + ... + return friends + + Note that as the method is now a property, in Python code it will need to + be invoked appropriately:: + + # in the view: + if person.friends: + + The cached value can be treated like an ordinary attribute of the instance:: + + # clear it, requiring re-computation next time it's called + del person.friends # or delattr(person, "friends") + + # set a value manually, that will persist on the instance until cleared + person.friends = ["Huckleberry Finn", "Tom Sawyer"] + + As well as offering potential performance advantages, ``@cached_property`` + can ensure that an attribute's value does not change unexpectedly over the + life of an instance. This could occur with a method whose computation is + based on ``datetime.now()``, or simply if a change were saved to the + database by some other process in the brief interval between subsequent + invocations of a method on the same instance. + .. function:: allow_lazy(func, *resultclasses) Django offers many utility functions (particularly in ``django.utils``) that diff --git a/docs/releases/1.2.1.txt b/docs/releases/1.2.1.txt index bdac39dd5d..0e193bfe34 100644 --- a/docs/releases/1.2.1.txt +++ b/docs/releases/1.2.1.txt @@ -4,7 +4,7 @@ Django 1.2.1 release notes Django 1.2.1 was released almost immediately after 1.2.0 to correct two small bugs: one was in the documentation packaging script, the other was a bug_ that -affected datetime form field widgets when localisation was enabled. +affected datetime form field widgets when localization was enabled. .. _bug: https://code.djangoproject.com/ticket/13560 diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 060f099c52..6bf4c0db60 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -365,7 +365,7 @@ In earlier Django versions, when a model instance containing a file from the backend storage. This opened the door to several data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.3, when a model is deleted the -:class:`~django.db.models.FileField`'s ``delete()`` method won't be called. If +:class:`~django.db.models.FileField`’s ``delete()`` method won't be called. If you need cleanup of orphaned files, you'll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron). @@ -654,7 +654,7 @@ Password reset view now accepts ``from_email`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :func:`django.contrib.auth.views.password_reset` view now accepts a -``from_email`` parameter, which is passed to the ``password_reset_form``'s +``from_email`` parameter, which is passed to the ``password_reset_form``’s ``save()`` method as a keyword argument. If you are using this view with a custom password reset form, then you will need to ensure your form's ``save()`` method accepts this keyword argument. diff --git a/docs/releases/1.5-beta-1.txt b/docs/releases/1.5-beta-1.txt index 69c591ac03..4495a281af 100644 --- a/docs/releases/1.5-beta-1.txt +++ b/docs/releases/1.5-beta-1.txt @@ -7,7 +7,7 @@ November 27, 2012. Welcome to Django 1.5 beta! This is the second in a series of preview/development releases leading -up to the eventual release of Django 1.5, scheduled for Decemeber +up to the eventual release of Django 1.5, scheduled for December 2012. This release is primarily targeted at developers who are interested in trying out new features and testing the Django codebase to help identify and resolve bugs prior to the final 1.5 release. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b8babe1843..c0f5c51194 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -32,6 +32,16 @@ deprecation process for some features`_. .. _`backwards incompatible changes`: `Backwards incompatible changes in 1.6`_ .. _`begun the deprecation process for some features`: `Features deprecated in 1.6`_ +Python compatibility +==================== + +Django 1.6, like Django 1.5, requires Python 2.6.5 or above. Python 3 is also +officially supported. We **highly recommend** the latest minor release for each +supported Python series (2.6.X, 2.7.X, 3.2.X, and 3.3.X). + +Django 1.6 will be the final release series to support Python 2.6; beginning +with Django 1.7, the minimum supported Python version will be 2.7. + What's new in Django 1.6 ======================== @@ -173,7 +183,7 @@ Minor features * The :attr:`~django.views.generic.edit.DeletionMixin.success_url` of :class:`~django.views.generic.edit.DeletionMixin` is now interpolated with - its ``object``\'s ``__dict__``. + its ``object``’s ``__dict__``. * :class:`~django.http.HttpResponseRedirect` and :class:`~django.http.HttpResponsePermanentRedirect` now provide an ``url`` @@ -199,10 +209,6 @@ Minor features * The admin list columns have a ``column-<field_name>`` class in the HTML so the columns header can be styled with CSS, e.g. to set a column width. -* Some admin templates now have ``app-<app_name>`` and ``model-<model_name>`` - classes in their ``<body>`` tag to allow customizing the CSS per app or per - model. - * The :ref:`isolation level<database-isolation-level>` can be customized under PostgreSQL. @@ -324,7 +330,7 @@ Minor features * :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 + :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``. @@ -654,7 +660,9 @@ will render something like: <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=''``. +the ``label_suffix``, instantiate the form ``label_suffix=''``. You can also +customize the ``label_suffix`` on a per-field basis using the new +``label_suffix`` parameter on :meth:`~django.forms.BoundField.label_tag`. Admin views ``_changelist_filters`` GET parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -801,6 +809,13 @@ Miscellaneous of the admin views. You should update your custom templates if they use the previous parameter name. +* :meth:`~django.core.validators.validate_email` now accepts email addresses + with ``localhost`` as the domain. + +* The :djadminopt:`--keep-pot` option was added to :djadmin:`makemessages` + to prevent django from deleting the temporary .pot file it generates before + creating the .po file. + Features deprecated in 1.6 ========================== diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index a617c90b34..23e4e3e64a 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -57,6 +57,13 @@ but a few of the key features are: will still work, but that method name is deprecated and you should change it as soon as possible (nothing more than renaming is required). +Calling custom ``QuerySet`` methods from the ``Manager`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`QuerySet.as_manager() <django.db.models.query.QuerySet.as_manager>` +class method has been added to :ref:`create Manager with QuerySet methods +<create-manager-with-queryset-methods>`. + Admin shortcuts support time zones ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -99,6 +106,58 @@ Minor features allowing the ``published`` element to be included in the feed (which relies on ``pubdate``). +* Buttons in :mod:`django.contrib.admin` now use the ``border-radius`` CSS + property for rounded corners rather than GIF background images. + +* Some admin templates now have ``app-<app_name>`` and ``model-<model_name>`` + classes in their ``<body>`` tag to allow customizing the CSS per app or per + model. + +* The admin changelist cells now have a ``field-<field_name>`` class in the + HTML to enable style customizations. + +* :func:`~django.core.mail.send_mail` now accepts an ``html_message`` + parameter for sending a multipart ``text/plain`` and ``text/html`` email. + +* The :djadminopt:`--no-color` option for ``django-admin.py`` allows you to + disable the colorization of management command output. + +* The :mod:`sitemap framework<django.contrib.sitemaps>` now makes use of + :attr:`~django.contrib.sitemaps.Sitemap.lastmod` to set a ``Last-Modified`` + header in the response. This makes it possible for the + :class:`~django.middleware.http.ConditionalGetMiddleware` to handle + conditional ``GET`` requests for sitemaps which set ``lastmod``. + +* You can override the new :meth:`AuthenticationForm.confirm_login_allowed() + <django.contrib.auth.forms.AuthenticationForm.confirm_login_allowed>` method + to more easily customize the login policy. + +* :attr:`Field.choices<django.db.models.Field.choices>` now allows you to + customize the "empty choice" label by including a tuple with an empty string + or ``None`` for the key and the custom label as the value. The default blank + option ``"----------"`` will be omitted in this case. + +* The admin's search fields can now be customized per-request thanks to the new + :meth:`django.contrib.admin.ModelAdmin.get_search_fields` method. + +* The :meth:`ModelAdmin.get_fields() + <django.contrib.admin.ModelAdmin.get_fields>` method may be overridden to + customize the value of :attr:`ModelAdmin.fields + <django.contrib.admin.ModelAdmin.fields>`. + +* :func:`django.contrib.auth.views.password_reset` takes an optional + ``html_email_template_name`` parameter used to send a multipart HTML email + for password resets. + +* :class:`~django.forms.MultiValueField` allows optional subfields by setting + the ``require_all_fields`` argument to ``False``. The ``required`` attribute + for each individual field will be respected, and a new ``incomplete`` + validation error will be raised when any required fields are empty. + +* The :meth:`~django.forms.Form.clean` method on a form no longer needs to + return ``self.cleaned_data``. If it does return a changed dictionary then + that will still be used. + Backwards incompatible changes in 1.7 ===================================== @@ -130,14 +189,31 @@ Miscellaneous have a custom :class:`~django.core.files.uploadhandler.FileUploadHandler` that implements ``new_file()``, be sure it accepts this new parameter. +* :class:`ModelFormSet<django.forms.models.BaseModelFormSet>`’s no longer + delete instances when ``save(commit=False)`` is called. See + :attr:`~django.forms.formsets.BaseFormSet.can_delete` for instructions on how + to manually delete objects from deleted forms. + +* Loading empty fixtures emits a ``RuntimeWarning`` rather than raising + :class:`~django.core.management.CommandError`. + +* :func:`django.contrib.staticfiles.views.serve` will now raise an + :exc:`~django.http.Http404` exception instead of + :exc:`~django.core.exceptions.ImproperlyConfigured` when :setting:`DEBUG` + is ``False``. This change removes the need to conditionally add the view to + your root URLconf, which in turn makes it safe to reverse by name. It also + removes the ability for visitors to generate spurious HTTP 500 errors by + requesting static files that don't exist or haven't been collected yet. + Features deprecated in 1.7 ========================== -``django.utils.dictconfig`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``django.utils.dictconfig``/``django.utils.importlib`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``django.utils.dictconfig`` was a copy of :mod:`logging.config` provided for -Python versions prior to 2.7. It has been deprecated. +``django.utils.dictconfig`` and ``django.utils.importlib`` were copies of +respectively :mod:`logging.config` and :mod:`importlib` provided for Python +versions prior to 2.7. They have been deprecated. ``django.utils.unittest`` ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -147,3 +223,27 @@ on all Python versions. Since ``unittest2`` became the standard library's :mod:`unittest` module in Python 2.7, and Django 1.7 drops support for older Python versions, this module isn't useful anymore. It has been deprecated. Use :mod:`unittest` instead. + +``django.utils.datastructures.SortedDict`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As :class:`~collections.OrderedDict` was added to the standard library in +Python 2.7, :class:`~django.utils.datastructures.SortedDict` is no longer +needed and has been deprecated. + +Custom SQL location for models package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, if models were organized in a package (``myapp/models/``) rather +than simply ``myapp/models.py``, Django would look for :ref:`initial SQL data +<initial-sql>` in ``myapp/models/sql/``. This bug has been fixed so that Django +will search ``myapp/sql/`` as documented. The old location will continue to +work until Django 1.9. + +``declared_fieldsets`` attribute on ``ModelAdmin.`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``ModelAdmin.declared_fieldsets`` was deprecated. Despite being a private API, +it will go through a regular deprecation path. This attribute was mostly used +by methods that bypassed ``ModelAdmin.get_fieldsets()`` but this was considered +a bug and has been addressed. diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index cfa3ec3cad..902adc9e32 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -561,13 +561,33 @@ Most built-in authentication views provide a URL name for easier reference. See patterns. -.. function:: login(request, [template_name, redirect_field_name, authentication_form]) +.. function:: login(request, [template_name, redirect_field_name, authentication_form, current_app, extra_context]) **URL name:** ``login`` See :doc:`the URL documentation </topics/http/urls>` for details on using named URL patterns. + **Optional arguments:** + + * ``template_name``: The name of a template to display for the view used to + log the user in. Defaults to :file:`registration/login.html`. + + * ``redirect_field_name``: The name of a ``GET`` field containing the + URL to redirect to after login. Overrides ``next`` if the given + ``GET`` parameter is passed. + + * ``authentication_form``: A callable (typically just a form class) to + use for authentication. Defaults to + :class:`~django.contrib.auth.forms.AuthenticationForm`. + + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + Here's what ``django.contrib.auth.views.login`` does: * If called via ``GET``, it displays a login form that POSTs to the @@ -657,7 +677,7 @@ patterns. .. _site framework docs: ../sites/ -.. function:: logout(request, [next_page, template_name, redirect_field_name]) +.. function:: logout(request, [next_page, template_name, redirect_field_name, current_app, extra_context]) Logs a user out. @@ -675,6 +695,13 @@ patterns. URL to redirect to after log out. Overrides ``next_page`` if the given ``GET`` parameter is passed. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``title``: The string "Logged out", localized. @@ -691,7 +718,14 @@ patterns. :attr:`request.META['SERVER_NAME'] <django.http.HttpRequest.META>`. For more on sites, see :doc:`/ref/contrib/sites`. -.. function:: logout_then_login(request[, login_url]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: logout_then_login(request[, login_url, current_app, extra_context]) Logs a user out, then redirects to the login page. @@ -702,7 +736,14 @@ patterns. * ``login_url``: The URL of the login page to redirect to. Defaults to :setting:`settings.LOGIN_URL <LOGIN_URL>` if not supplied. -.. function:: password_change(request[, template_name, post_change_redirect, password_change_form]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_change(request[, template_name, post_change_redirect, password_change_form, current_app, extra_context]) Allows a user to change their password. @@ -722,11 +763,18 @@ patterns. actually changing the user's password. Defaults to :class:`~django.contrib.auth.forms.PasswordChangeForm`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``form``: The password change form (see ``password_change_form`` above). -.. function:: password_change_done(request[, template_name]) +.. function:: password_change_done(request[, template_name, current_app, extra_context]) The page shown after a user has changed their password. @@ -738,7 +786,14 @@ patterns. Defaults to :file:`registration/password_change_done.html` if not supplied. -.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email, current_app, extra_context, html_email_template_name]) Allows a user to reset their password by generating a one-time use link that can be used to reset the password, and sending that link to the @@ -794,6 +849,21 @@ patterns. * ``from_email``: A valid email address. By default Django uses the :setting:`DEFAULT_FROM_EMAIL`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + + * ``html_email_template_name``: The full name of a template to use + for generating a ``text/html`` multipart email with the password reset + link. By default, HTML email is not sent. + + .. versionadded:: 1.7 + + ``html_email_template_name`` was added. + **Template context:** * ``form``: The form (see ``password_reset_form`` above) for resetting @@ -838,7 +908,7 @@ patterns. single line plain text string. -.. function:: password_reset_done(request[, template_name]) +.. function:: password_reset_done(request[, template_name, current_app, extra_context]) The page shown after a user has been emailed a link to reset their password. This view is called by default if the :func:`password_reset` view @@ -852,7 +922,14 @@ patterns. Defaults to :file:`registration/password_reset_done.html` if not supplied. -.. function:: password_reset_confirm(request[, uidb64, token, template_name, token_generator, set_password_form, post_reset_redirect]) + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + +.. function:: password_reset_confirm(request[, uidb64, token, template_name, token_generator, set_password_form, post_reset_redirect, current_app, extra_context]) Presents a form for entering a new password. @@ -883,6 +960,13 @@ patterns. * ``post_reset_redirect``: URL to redirect after the password reset done. Defaults to ``None``. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + **Template context:** * ``form``: The form (see ``set_password_form`` above) for setting the @@ -891,7 +975,7 @@ patterns. * ``validlink``: Boolean, True if the link (combination of ``uidb64`` and ``token``) is valid or unused yet. -.. function:: password_reset_complete(request[,template_name]) +.. function:: password_reset_complete(request[,template_name, current_app, extra_context]) Presents a view which informs the user that the password has been successfully changed. @@ -903,6 +987,13 @@ patterns. * ``template_name``: The full name of a template to display the view. Defaults to :file:`registration/password_reset_complete.html`. + * ``current_app``: A hint indicating which application contains the current + view. See the :ref:`namespaced URL resolution strategy + <topics-http-reversing-url-namespaces>` for more information. + + * ``extra_context``: A dictionary of context data that will be added to the + default context data passed to the template. + Helper functions ---------------- @@ -959,6 +1050,40 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`: Takes ``request`` as its first positional argument, which is stored on the form instance for use by sub-classes. + .. method:: confirm_login_allowed(user) + + .. versionadded:: 1.7 + + By default, ``AuthenticationForm`` rejects users whose ``is_active`` flag + is set to ``False``. You may override this behavior with a custom policy to + determine which users can log in. Do this with a custom form that subclasses + ``AuthenticationForm`` and overrides the ``confirm_login_allowed`` method. + This method should raise a :exc:`~django.core.exceptions.ValidationError` + if the given user may not log in. + + For example, to allow all users to log in, regardless of "active" status:: + + from django.contrib.auth.forms import AuthenticationForm + + class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm): + def confirm_login_allowed(self, user): + pass + + Or to allow only some active users to log in:: + + class PickyAuthenticationForm(AuthenticationForm): + def confirm_login_allowed(self, user): + if not user.is_active: + raise forms.ValidationError( + _("This account is inactive."), + code='inactive', + ) + if user.username.startswith('b'): + raise forms.ValidationError( + _("Sorry, accounts starting with 'b' aren't welcome here."), + code='no_b_users', + ) + .. class:: PasswordChangeForm A form for allowing a user to change their password. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index ea23943c81..092df1f876 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -39,6 +39,8 @@ Django also works well with "upstream" caches, such as `Squid caches that you don't directly control but to which you can provide hints (via HTTP headers) about which parts of your site should be cached, and how. +.. _setting-up-the-cache: + Setting up the cache ==================== @@ -152,6 +154,8 @@ permanent storage -- they're all intended to be solutions for caching, not storage -- but we point this out here because memory-based caching is particularly temporary. +.. _database-caching: + Database caching ---------------- diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index df1a5505a5..fdd5d42c5d 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -170,7 +170,7 @@ being taken from the :attr:`~django.views.generic.list.MultipleObjectTemplateResponseMixin.template_name_suffix` attribute. (The date based generic views use suffixes such as ``_archive``, ``_archive_year`` and so on to use different templates for the various -specialised date-based list views.) +specialized date-based list views.) Using Django's class-based view mixins ====================================== @@ -288,7 +288,7 @@ object. In order to do this, we need to have two different querysets: ``Book`` queryset for use by :class:`~django.views.generic.list.ListView` Since we have access to the ``Publisher`` whose books we want to list, we - simply override ``get_queryset()`` and use the ``Publisher``'s + simply override ``get_queryset()`` and use the ``Publisher``’s :ref:`reverse foreign key manager<backwards-related-objects>`. ``Publisher`` queryset for use in :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` @@ -345,7 +345,7 @@ The ``paginate_by`` is deliberately small in the example so you don't have to create lots of books to see the pagination working! Here's the template you'd want to use: -.. code-block: html+django +.. code-block:: html+django {% extends "base.html" %} diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index b940b09d33..2d3f35db8b 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -100,7 +100,7 @@ access ``self.model`` to get the model class to which they're attached. Modifying initial Manager QuerySets ----------------------------------- -A ``Manager``'s base ``QuerySet`` returns all objects in the system. For +A ``Manager``’s base ``QuerySet`` returns all objects in the system. For example, using this model:: from django.db import models @@ -111,7 +111,7 @@ example, using this model:: ...the statement ``Book.objects.all()`` will return all books in the database. -You can override a ``Manager``\'s base ``QuerySet`` by overriding the +You can override a ``Manager``’s base ``QuerySet`` by overriding the ``Manager.get_queryset()`` method. ``get_queryset()`` should return a ``QuerySet`` with the properties you require. @@ -201,6 +201,125 @@ attribute on the manager class. This is documented fully below_. .. _below: manager-types_ +.. _calling-custom-queryset-methods-from-manager: + +Calling custom ``QuerySet`` methods from the ``Manager`` +-------------------------------------------------------- + +While most methods from the standard ``QuerySet`` are accessible directly from +the ``Manager``, this is only the case for the extra methods defined on a +custom ``QuerySet`` if you also implement them on the ``Manager``:: + + class PersonQuerySet(models.QuerySet): + def male(self): + return self.filter(sex='M') + + def female(self): + return self.filter(sex='F') + + class PersonManager(models.Manager): + def get_queryset(self): + return PersonQuerySet() + + def male(self): + return self.get_queryset().male() + + def female(self): + return self.get_queryset().female() + + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female'))) + people = PersonManager() + +This example allows you to call both ``male()`` and ``female()`` directly from +the manager ``Person.people``. + +.. _create-manager-with-queryset-methods: + +Creating ``Manager`` with ``QuerySet`` methods +---------------------------------------------- + +.. versionadded:: 1.7 + +In lieu of the above approach which requires duplicating methods on both the +``QuerySet`` and the ``Manager``, :meth:`QuerySet.as_manager() +<django.db.models.query.QuerySet.as_manager>` can be used to create an instance +of ``Manager`` with a copy of a custom ``QuerySet``’s methods:: + + class Person(models.Model): + ... + people = PersonQuerySet.as_manager() + +The ``Manager`` instance created by :meth:`QuerySet.as_manager() +<django.db.models.query.QuerySet.as_manager>` will be virtually +identical to the ``PersonManager`` from the previous example. + +Not every ``QuerySet`` method makes sense at the ``Manager`` level; for +instance we intentionally prevent the :meth:`QuerySet.delete() +<django.db.models.query.QuerySet.delete>` method from being copied onto +the ``Manager`` class. + +Methods are copied according to the following rules: + +- Public methods are copied by default. +- Private methods (starting with an underscore) are not copied by default. +- Methods with a `queryset_only` attribute set to `False` are always copied. +- Methods with a `queryset_only` attribute set to `True` are never copied. + +For example:: + + class CustomQuerySet(models.QuerySet): + # Available on both Manager and QuerySet. + def public_method(self): + return + + # Available only on QuerySet. + def _private_method(self): + return + + # Available only on QuerySet. + def opted_out_public_method(self): + return + opted_out_public_method.queryset_only = True + + # Available on both Manager and QuerySet. + def _opted_in_private_method(self): + return + _opted_in_private_method.queryset_only = False + +from_queryset +~~~~~~~~~~~~~ + +.. classmethod:: from_queryset(queryset_class) + +For advance usage you might want both a custom ``Manager`` and a custom +``QuerySet``. You can do that by calling ``Manager.from_queryset()`` which +returns a *subclass* of your base ``Manager`` with a copy of the custom +``QuerySet`` methods:: + + class BaseManager(models.Manager): + def __init__(self, *args, **kwargs): + ... + + def manager_only_method(self): + return + + class CustomQuerySet(models.QuerySet): + def manager_and_queryset_method(self): + return + + class MyModel(models.Model): + objects = BaseManager.from_queryset(CustomQueryset)(*args, **kwargs) + +You may also store the generated class into a variable:: + + CustomManager = BaseManager.from_queryset(CustomQueryset) + + class MyModel(models.Model): + objects = CustomManager(*args, **kwargs) + .. _custom-managers-and-inheritance: Custom managers and model inheritance diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 1d6052f938..9a0d0ce6b9 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -720,7 +720,7 @@ efficient code. In a newly created :class:`~django.db.models.query.QuerySet`, the cache is empty. The first time a :class:`~django.db.models.query.QuerySet` is evaluated -- and, hence, a database query happens -- Django saves the query results in -the :class:`~django.db.models.query.QuerySet`\'s cache and returns the results +the :class:`~django.db.models.query.QuerySet`’s cache and returns the results that have been explicitly requested (e.g., the next element, if the :class:`~django.db.models.query.QuerySet` is being iterated over). Subsequent evaluations of the :class:`~django.db.models.query.QuerySet` reuse the cached diff --git a/docs/topics/db/tablespaces.txt b/docs/topics/db/tablespaces.txt index 8bf1d07bca..115887f512 100644 --- a/docs/topics/db/tablespaces.txt +++ b/docs/topics/db/tablespaces.txt @@ -30,7 +30,7 @@ Declaring tablespaces for indexes --------------------------------- You can pass the :attr:`~django.db.models.Field.db_tablespace` option to a -``Field`` constructor to specify an alternate tablespace for the ``Field``'s +``Field`` constructor to specify an alternate tablespace for the ``Field``’s column index. If no index would be created for the column, the option is ignored. diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 903579cc38..4411b326d7 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -619,7 +619,7 @@ context managers breaks atomicity. Managing autocommit ~~~~~~~~~~~~~~~~~~~ -Django 1.6 introduces an explicit :ref:`API for mananging autocommit +Django 1.6 introduces an explicit :ref:`API for managing autocommit <managing-autocommit>`. To disable autocommit temporarily, instead of:: diff --git a/docs/topics/email.txt b/docs/topics/email.txt index 8bf501c62a..c007c2b856 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -38,7 +38,7 @@ a secure connection is used. send_mail() =========== -.. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None) +.. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None) The simplest way to send email is using ``django.core.mail.send_mail()``. @@ -66,6 +66,14 @@ are required. If unspecified, an instance of the default backend will be used. See the documentation on :ref:`Email backends <topic-email-backends>` for more details. +* ``html_message``: If ``html_message`` is provided, the resulting email will be a + :mimetype:`multipart/alternative` email with ``message`` as the + :mimetype:`text/plain` content type and ``html_message`` as the + :mimetype:`text/html` content type. + +.. versionadded:: 1.7 + + The ``html_message`` parameter was added. send_mass_mail() ================ diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 470d9f52e4..8be6e10ff6 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -3,7 +3,10 @@ Formsets ======== -.. class:: django.forms.formsets.BaseFormSet +.. module:: django.forms.formsets + :synopsis: An abstraction for working with multiple forms on the same page. + +.. class:: 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,9 +167,7 @@ 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) +.. method:: BaseFormSet.total_error_count(self) .. versionadded:: 1.6 @@ -353,6 +354,8 @@ formsets and deletion of forms from a formset. ``can_order`` ~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_order + Default: ``False`` Lets you create a formset with the ability to order:: @@ -411,6 +414,8 @@ happen when the user changes these values:: ``can_delete`` ~~~~~~~~~~~~~~ +.. attribute:: BaseFormSet.can_delete + Default: ``False`` Lets you create a formset with the ability to select forms for deletion:: @@ -463,10 +468,23 @@ delete fields you can access them with ``deleted_forms``:: If you are using a :class:`ModelFormSet<django.forms.models.BaseModelFormSet>`, model instances for deleted forms will be deleted when you call -``formset.save()``. On the other hand, if you are using a plain ``FormSet``, -it's up to you to handle ``formset.deleted_forms``, perhaps in your formset's -``save()`` method, as there's no general notion of what it means to delete a -form. +``formset.save()``. + +.. versionchanged:: 1.7 + + If you call ``formset.save(commit=False)``, objects will not be deleted + automatically. You'll need to call ``delete()`` on each of the + :attr:`formset.deleted_objects + <django.forms.models.BaseModelFormSet.deleted_objects>` to actually delete + them:: + + >>> instances = formset.save(commit=False) + >>> for obj in formset.deleted_objects: + ... obj.delete() + +On the other hand, if you are using a plain ``FormSet``, it's up to you to +handle ``formset.deleted_forms``, perhaps in your formset's ``save()`` method, +as there's no general notion of what it means to delete a form. Adding additional fields to a formset ------------------------------------- diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 0f3c5bb815..a823c8acb5 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -392,7 +392,7 @@ these security concerns do not apply to you: model = Author fields = '__all__' -2. Set the ``exclude`` attribute of the ``ModelForm``'s inner ``Meta`` class to +2. Set the ``exclude`` attribute of the ``ModelForm``’s inner ``Meta`` class to a list of fields to be excluded from the form. For example:: @@ -757,7 +757,7 @@ Specifying widgets to use in the form with ``widgets`` .. versionadded:: 1.6 Using the ``widgets`` parameter, you can specify a dictionary of values to -customize the ``ModelForm``'s widget class for a particular field. This +customize the ``ModelForm``’s widget class for a particular field. This works the same way as the ``widgets`` dictionary on the inner ``Meta`` class of a ``ModelForm`` works:: @@ -825,6 +825,13 @@ to the database. If your formset contains a ``ManyToManyField``, you'll also need to call ``formset.save_m2m()`` to ensure the many-to-many relationships are saved properly. +After calling ``save()``, your model formset will have three new attributes +containing the formset's changes: + +.. attribute:: models.BaseModelFormSet.changed_objects +.. attribute:: models.BaseModelFormSet.deleted_objects +.. attribute:: models.BaseModelFormSet.new_objects + .. _model-formsets-max-num: Limiting the number of editable objects diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 503d4322e0..6bb7ccb8f8 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -28,11 +28,12 @@ here's the default value created by :djadmin:`django-admin.py startproject <startproject>`:: MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) A Django installation doesn't require any middleware — diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 0b818c4f5d..3ee7af9539 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -293,7 +293,7 @@ You can edit it multiple times. expiration (or those set to expire at browser close), this will equal the date :setting:`SESSION_COOKIE_AGE` seconds from now. - This function accepts the same keyword argumets as :meth:`get_expiry_age`. + This function accepts the same keyword arguments as :meth:`get_expiry_age`. .. method:: get_expire_at_browser_close diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 8a3f240307..83610e99c0 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -412,7 +412,7 @@ For example:: ) In this example, for a request to ``/blog/2005/``, Django will call -``blog.views.year_archive(year='2005', foo='bar')``. +``blog.views.year_archive(request, year='2005', foo='bar')``. This technique is used in the :doc:`syndication framework </ref/contrib/syndication>` to pass metadata and @@ -623,7 +623,7 @@ change the entry in the URLconf. In some scenarios where views are of a generic nature, a many-to-one relationship might exist between URLs and views. For these cases the view name -isn't a good enough identificator for it when it comes the time of reversing +isn't a good enough identifier for it when comes the time of reversing URLs. Read the next section to know about the solution Django provides for this. .. _naming-url-patterns: diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 5c27c9c958..ebe5302ef8 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -140,18 +140,18 @@ The 404 (page not found) view .. function:: django.views.defaults.page_not_found(request, template_name='404.html') -When you raise an ``Http404`` exception, Django loads a special view devoted -to handling 404 errors. By default, it's the view -``django.views.defaults.page_not_found``, which either produces a very simple -"Not Found" message or loads and renders the template ``404.html`` if you -created it in your root template directory. +When you raise :exc:`~django.http.Http404` from within a view, Django loads a +special view devoted to handling 404 errors. By default, it's the view +:func:`django.views.defaults.page_not_found`, which either produces a very +simple "Not Found" message or loads and renders the template ``404.html`` if +you created it in your root template directory. The default 404 view will pass one variable to the template: ``request_path``, which is the URL that resulted in the error. The ``page_not_found`` view should suffice for 99% of Web applications, but if -you want to override it, you can specify ``handler404`` in your URLconf, like -so:: +you want to override it, you can specify ``handler404`` in your root URLconf +(setting ``handler404`` anywhere else will have no effect), like so:: handler404 = 'mysite.views.my_custom_404_view' diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 5d88ed4f82..3e813b2a41 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -59,11 +59,10 @@ installed. If you can't use mod_wsgi for some reason, fear not: Django supports many other deployment options. One is :doc:`uWSGI </howto/deployment/wsgi/uwsgi>`; it works -very well with `nginx`_. Another is :doc:`FastCGI </howto/deployment/fastcgi>`, -perfect for using Django with servers other than Apache. Additionally, Django -follows the WSGI spec (:pep:`3333`), which allows it to run on a variety of -server platforms. See the `server-arrangements wiki page`_ for specific -installation instructions for each platform. +very well with `nginx`_. Additionally, Django follows the WSGI spec +(:pep:`3333`), which allows it to run on a variety of server platforms. See the +`server-arrangements wiki page`_ for specific installation instructions for +each platform. .. _Apache: http://httpd.apache.org/ .. _nginx: http://nginx.org/ diff --git a/docs/topics/localflavor.txt b/docs/topics/localflavor.txt index 8bc8b2f9e3..724805b147 100644 --- a/docs/topics/localflavor.txt +++ b/docs/topics/localflavor.txt @@ -7,8 +7,9 @@ assorted pieces of code that are useful for particular countries or cultures. This code is now distributed separately from Django, for easier maintenance and to trim the size of Django's codebase. -The localflavor packages are named ``django-localflavor-*``, where the asterisk -is an `ISO 3166 country code`_. For example: ``django-localflavor-us`` is the +The new localflavor package is named ``django-localflavor``, with a main +module called ``localflavor`` and many subpackages using an +`ISO 3166 country code`_. For example: ``localflavor.us`` is the localflavor package for the U.S.A. Most of these ``localflavor`` add-ons are country-specific fields for the @@ -22,7 +23,7 @@ For example, here's how you can create a form with a field representing a French telephone number:: from django import forms - from django_localflavor_fr.forms import FRPhoneNumberField + from localflavor.fr.forms import FRPhoneNumberField class MyForm(forms.Form): my_french_phone_no = FRPhoneNumberField() @@ -37,75 +38,30 @@ file. Supported countries =================== -The following countries have django-localflavor- packages. +See the official documentation for more information: -* Argentina: https://github.com/django/django-localflavor-ar -* Australia: https://github.com/django/django-localflavor-au -* Austria: https://github.com/django/django-localflavor-at -* Belgium: https://github.com/django/django-localflavor-be -* Brazil: https://github.com/django/django-localflavor-br -* Canada: https://github.com/django/django-localflavor-ca -* Chile: https://github.com/django/django-localflavor-cl -* China: https://github.com/django/django-localflavor-cn -* Colombia: https://github.com/django/django-localflavor-co -* Croatia: https://github.com/django/django-localflavor-hr -* Czech Republic: https://github.com/django/django-localflavor-cz -* Ecuador: https://github.com/django/django-localflavor-ec -* Finland: https://github.com/django/django-localflavor-fi -* France: https://github.com/django/django-localflavor-fr -* Germany: https://github.com/django/django-localflavor-de -* Greece: https://github.com/spapas/django-localflavor-gr -* Hong Kong: https://github.com/django/django-localflavor-hk -* Iceland: https://github.com/django/django-localflavor-is -* India: https://github.com/django/django-localflavor-in -* Indonesia: https://github.com/django/django-localflavor-id -* Ireland: https://github.com/django/django-localflavor-ie -* Israel: https://github.com/django/django-localflavor-il -* Italy: https://github.com/django/django-localflavor-it -* Japan: https://github.com/django/django-localflavor-jp -* Kuwait: https://github.com/django/django-localflavor-kw -* Lithuania: https://github.com/simukis/django-localflavor-lt -* Macedonia: https://github.com/django/django-localflavor-mk -* Mexico: https://github.com/django/django-localflavor-mx -* The Netherlands: https://github.com/django/django-localflavor-nl -* Norway: https://github.com/django/django-localflavor-no -* Peru: https://github.com/django/django-localflavor-pe -* Poland: https://github.com/django/django-localflavor-pl -* Portugal: https://github.com/django/django-localflavor-pt -* Paraguay: https://github.com/django/django-localflavor-py -* Romania: https://github.com/django/django-localflavor-ro -* Russia: https://github.com/django/django-localflavor-ru -* Slovakia: https://github.com/django/django-localflavor-sk -* Slovenia: https://github.com/django/django-localflavor-si -* South Africa: https://github.com/django/django-localflavor-za -* Spain: https://github.com/django/django-localflavor-es -* Sweden: https://github.com/django/django-localflavor-se -* Switzerland: https://github.com/django/django-localflavor-ch -* Turkey: https://github.com/django/django-localflavor-tr -* United Kingdom: https://github.com/django/django-localflavor-gb -* United States of America: https://github.com/django/django-localflavor-us -* Uruguay: https://github.com/django/django-localflavor-uy + https://django-localflavor.readthedocs.org/ Internationalization of localflavors ==================================== -To activate translations for a ``localflavor`` application, you must include -the application's name (e.g. ``django_localflavor_jp``) in the -:setting:`INSTALLED_APPS` setting, so the internationalization system can find -the catalog, as explained in :ref:`how-django-discovers-translations`. +To activate translations for the ``localflavor`` application, you must include +the application's name in the :setting:`INSTALLED_APPS` setting, so the +internationalization system can find the catalog, as explained in +:ref:`how-django-discovers-translations`. .. _localflavor-how-to-migrate: How to migrate ============== -If you've used the old ``django.contrib.localflavor`` package, follow these two -easy steps to update your code: +If you've used the old ``django.contrib.localflavor`` package or one of the +temporary ``django-localflavor-*`` releases, follow these two easy steps to +update your code: -1. Install the appropriate third-party ``django-localflavor-*`` package(s). - Go to https://github.com/django/ and find the package for your country. +1. Install the third-party ``django-localflavor`` package from PyPI. -2. Change your app's import statements to reference the new packages. +2. Change your app's import statements to reference the new package. For example, change this:: @@ -113,9 +69,9 @@ easy steps to update your code: ...to this:: - from django_localflavor_fr.forms import FRPhoneNumberField + from localflavor.fr.forms import FRPhoneNumberField -The code in the new packages is the same (it was copied directly from Django), +The code in the new package is the same (it was copied directly from Django), so you don't have to worry about backwards compatibility in terms of functionality. Only the imports have changed. diff --git a/docs/topics/templates.txt b/docs/topics/templates.txt index 58a3ee9870..c75b83f158 100644 --- a/docs/topics/templates.txt +++ b/docs/topics/templates.txt @@ -45,7 +45,9 @@ A template contains **variables**, which get replaced with values when the template is evaluated, and **tags**, which control the logic of the template. Below is a minimal template that illustrates a few basics. Each element will be -explained later in this document.:: +explained later in this document. + +.. code-block:: html+django {% extends "base_generic.html" %} diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 6d9ea8d5c1..99af951866 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -174,7 +174,7 @@ Advanced features of ``TransactionTestCase`` .. 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 + a deprecation period in the future, for instance to accommodate changes in application loading. It's used to optimize Django's own test suite, which contains hundreds diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index dbeaf0fb00..457d10eec0 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -1421,7 +1421,7 @@ The decorator can also be applied to test case classes:: You can also simulate the absence of a setting by deleting it after settings -have been overriden, like this:: +have been overridden, like this:: @override_settings() def test_something(self): @@ -1437,7 +1437,7 @@ callbacks to clean up and otherwise reset state when settings are changed. Django itself uses this signal to reset various data: ================================ ======================== -Overriden settings Data reset +Overridden settings Data reset ================================ ======================== USE_TZ, TIME_ZONE Databases timezone TEMPLATE_CONTEXT_PROCESSORS Context processors cache @@ -1639,7 +1639,7 @@ your test suite. .. versionadded:: 1.5 Asserts that the strings ``xml1`` and ``xml2`` are equal. The - comparison is based on XML semantics. Similarily to + comparison is based on XML semantics. Similarly to :meth:`~SimpleTestCase.assertHTMLEqual`, the comparison is made on parsed content, hence only semantic differences are considered, not syntax differences. When unvalid XML is passed in any parameter, an diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 175b1972c9..a63f2b60ce 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from django.core.paginator import Paginator @@ -107,3 +105,10 @@ class DynamicListFilterChildAdmin(admin.ModelAdmin): my_list_filter.remove('parent') return my_list_filter +class DynamicSearchFieldsChildAdmin(admin.ModelAdmin): + search_fields = ('name',) + + def get_search_fields(self, request): + search_fields = super(DynamicSearchFieldsChildAdmin, self).get_search_fields(request) + search_fields += ('age',) + return search_fields diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 7f3f0d162e..cb1ac5039f 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime @@ -18,7 +18,8 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin) + SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin, + DynamicSearchFieldsChildAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -91,7 +92,7 @@ class ChangeListTests(TestCase): context = Context({'cl': cl}) table_output = template.render(context) link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) - row_html = '<tbody><tr class="row1"><th><a href="%s">name</a></th><td class="nowrap">(None)</td></tr></tbody>' % link + row_html = '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th><td class="field-parent nowrap">(None)</td></tr></tbody>' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -114,7 +115,7 @@ class ChangeListTests(TestCase): context = Context({'cl': cl}) table_output = template.render(context) link = reverse('admin:admin_changelist_child_change', args=(new_child.id,)) - row_html = '<tbody><tr class="row1"><th><a href="%s">name</a></th><td class="nowrap">Parent object</td></tr></tbody>' % link + row_html = '<tbody><tr class="row1"><th class="field-name"><a href="%s">name</a></th><td class="field-parent nowrap">Parent object</td></tr></tbody>' % link self.assertFalse(table_output.find(row_html) == -1, 'Failed to find expected row element: %s' % table_output) @@ -150,7 +151,7 @@ class ChangeListTests(TestCase): # make sure that list editable fields are rendered in divs correctly editable_name_field = '<input name="form-0-name" value="name" class="vTextField" maxlength="30" type="text" id="id_form-0-name" />' - self.assertInHTML('<td>%s</td>' % editable_name_field, table_output, msg_prefix='Failed to find "name" list_editable field') + self.assertInHTML('<td class="field-name">%s</td>' % editable_name_field, table_output, msg_prefix='Failed to find "name" list_editable field') def test_result_list_editable(self): """ @@ -588,6 +589,13 @@ class ChangeListTests(TestCase): response = m.changelist_view(request) self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age')) + def test_dynamic_search_fields(self): + child = self._create_superuser('child') + m = DynamicSearchFieldsChildAdmin(Child, admin.site) + request = self._mocked_authenticated_request('/child/', child) + response = m.changelist_view(request) + self.assertEqual(response.context_data['cl'].search_fields, ('name', 'age')) + def test_pagination_page_range(self): """ Regression tests for ticket #15653: ensure the number of pages diff --git a/tests/admin_custom_urls/tests.py b/tests/admin_custom_urls/tests.py index 31c93410f4..257638afb1 100644 --- a/tests/admin_custom_urls/tests.py +++ b/tests/admin_custom_urls/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import warnings from django.contrib.admin.util import quote diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py index 3c3a8fe5d8..914b4836e0 100644 --- a/tests/admin_docs/urls.py +++ b/tests/admin_docs/urls.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import absolute_import - from django.conf.urls import patterns from . import views diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index f05e8e2011..5e6b122fec 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 62f9e04e5b..c69800851a 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from django import forms diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 465b224d4f..f62a0c1e01 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase from django.contrib.admin.helpers import InlineAdminForm diff --git a/tests/admin_inlines/urls.py b/tests/admin_inlines/urls.py index cf18ef97cf..a5d927e20b 100644 --- a/tests/admin_inlines/urls.py +++ b/tests/admin_inlines/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, include from . import admin diff --git a/tests/admin_ordering/tests.py b/tests/admin_ordering/tests.py index 6655ad37ad..763e97bd72 100644 --- a/tests/admin_ordering/tests.py +++ b/tests/admin_ordering/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.test import TestCase, RequestFactory from django.contrib import admin diff --git a/tests/admin_registration/tests.py b/tests/admin_registration/tests.py index 1b2d291691..0e444fd2af 100644 --- a/tests/admin_registration/tests.py +++ b/tests/admin_registration/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.contrib import admin from django.core.exceptions import ImproperlyConfigured diff --git a/tests/admin_scripts/complex_app/admin/foo.py b/tests/admin_scripts/complex_app/admin/foo.py index 1ed704a66b..09ceba05aa 100644 --- a/tests/admin_scripts/complex_app/admin/foo.py +++ b/tests/admin_scripts/complex_app/admin/foo.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from ..models.foo import Foo diff --git a/tests/admin_scripts/complex_app/models/bar.py b/tests/admin_scripts/complex_app/models/bar.py index 15956f7a50..92f1b98694 100644 --- a/tests/admin_scripts/complex_app/models/bar.py +++ b/tests/admin_scripts/complex_app/models/bar.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.db import models from ..admin import foo diff --git a/tests/admin_scripts/management/commands/color_command.py b/tests/admin_scripts/management/commands/color_command.py new file mode 100644 index 0000000000..02da0a14ab --- /dev/null +++ b/tests/admin_scripts/management/commands/color_command.py @@ -0,0 +1,9 @@ +from django.core.management.base import NoArgsCommand + + +class Command(NoArgsCommand): + help = "Test color output" + requires_model_validation = False + + def handle_noargs(self, **options): + return self.style.SQL_KEYWORD('BEGIN') diff --git a/tests/admin_scripts/simple_app/models.py b/tests/admin_scripts/simple_app/models.py index b89f4b898b..e5b9e297c5 100644 --- a/tests/admin_scripts/simple_app/models.py +++ b/tests/admin_scripts/simple_app/models.py @@ -1,3 +1 @@ -from __future__ import absolute_import - from ..complex_app.models.bar import Bar diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 28f2dcb841..810c90c53e 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -18,7 +18,7 @@ import unittest import django from django import conf, get_version from django.conf import settings -from django.core.management import BaseCommand, CommandError +from django.core.management import BaseCommand, CommandError, call_command from django.db import connection from django.test.runner import DiscoverRunner from django.utils.encoding import force_text @@ -45,6 +45,7 @@ class AdminScriptTestCase(unittest.TestCase): settings_file_path = os.path.join(test_dir, filename) with open(settings_file_path, 'w') as settings_file: + settings_file.write('# -*- coding: utf-8 -*\n') settings_file.write('# Settings file automatically generated by admin_scripts test case\n') exports = [ 'DATABASES', @@ -921,14 +922,21 @@ class ManageAlternateSettings(AdminScriptTestCase): "alternate: manage.py can execute user commands if settings are provided as argument" args = ['noargs_command', '--settings=alternate_settings'] out, err = self.run_manage(args) - self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") self.assertNoOutput(err) def test_custom_command_with_environment(self): "alternate: manage.py can execute user commands if settings are provided in environment" args = ['noargs_command'] out, err = self.run_manage(args, 'alternate_settings') - self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertNoOutput(err) + + def test_custom_command_output_color(self): + "alternate: manage.py output syntax color can be deactivated with the `--no-color` option" + args = ['noargs_command', '--no-color', '--settings=alternate_settings'] + out, err = self.run_manage(args) + self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', True), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") self.assertNoOutput(err) @@ -1271,40 +1279,47 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s).") + def test_no_color(self): + "--no-color prevent colorization of the output" + out = StringIO() + + call_command('color_command', no_color=True, stdout=out) + self.assertEqual(out.getvalue(), 'BEGIN\n') + def test_base_command(self): "User BaseCommands can execute when a label is provided" args = ['base_command', 'testlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_base_command_no_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=(), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=(), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_base_command_multiple_label(self): "User BaseCommands can execute when no labels are provided" args = ['base_command', 'testlabel', 'anotherlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel', 'anotherlabel'), options=[('no_color', False), ('option_a', '1'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_base_command_with_option(self): "User BaseCommands can execute with options when a label is provided" args = ['base_command', 'testlabel', '--option_a=x'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_base_command_with_options(self): "User BaseCommands can execute with multiple options when a label is provided" args = ['base_command', 'testlabel', '-a', 'x', '--option_b=y'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_base_run_from_argv(self): """ @@ -1350,7 +1365,7 @@ class CommandTypes(AdminScriptTestCase): args = ['noargs_command'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:NoArgsCommand options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_noargs_with_args(self): "NoArg Commands raise an error if an argument is provided" @@ -1365,7 +1380,7 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") self.assertOutput(out, os.sep.join(['django', 'contrib', 'auth', 'models.py'])) - self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "'>, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_app_command_no_apps(self): "User AppCommands raise an error when no app name is provided" @@ -1380,10 +1395,10 @@ class CommandTypes(AdminScriptTestCase): self.assertNoOutput(err) self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.auth.models'") self.assertOutput(out, os.sep.join(['django', 'contrib', 'auth', 'models.py'])) - self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "'>, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") self.assertOutput(out, "EXECUTE:AppCommand app=<module 'django.contrib.contenttypes.models'") self.assertOutput(out, os.sep.join(['django', 'contrib', 'contenttypes', 'models.py'])) - self.assertOutput(out, "'>, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "'>, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_app_command_invalid_appname(self): "User AppCommands can execute when a single app name is provided" @@ -1402,7 +1417,7 @@ class CommandTypes(AdminScriptTestCase): args = ['label_command', 'testlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") def test_label_command_no_label(self): "User LabelCommands raise an error if no label is provided" @@ -1415,8 +1430,8 @@ class CommandTypes(AdminScriptTestCase): args = ['label_command', 'testlabel', 'anotherlabel'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") - self.assertOutput(out, "EXECUTE:LabelCommand label=anotherlabel, options=[('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:LabelCommand label=testlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:LabelCommand label=anotherlabel, options=[('no_color', False), ('pythonpath', None), ('settings', None), ('traceback', None), ('verbosity', '1')]") class ArgumentOrder(AdminScriptTestCase): """Tests for 2-stage argument parsing scheme. @@ -1440,35 +1455,35 @@ class ArgumentOrder(AdminScriptTestCase): args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") def test_setting_then_short_option(self): "Short options passed after settings are correctly handled" args = ['base_command', 'testlabel', '--settings=alternate_settings', '--option_a=x'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") def test_option_then_setting(self): "Options passed before settings are correctly handled" args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") def test_short_option_then_setting(self): "Short options passed before settings are correctly handled" args = ['base_command', 'testlabel', '-a', 'x', '--settings=alternate_settings'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', '2'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") def test_option_then_setting_then_option(self): "Options are correctly handled when they are passed before and after a setting" args = ['base_command', 'testlabel', '--option_a=x', '--settings=alternate_settings', '--option_b=y'] out, err = self.run_manage(args) self.assertNoOutput(err) - self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") + self.assertOutput(out, "EXECUTE:BaseCommand labels=('testlabel',), options=[('no_color', False), ('option_a', 'x'), ('option_b', 'y'), ('option_c', '3'), ('pythonpath', None), ('settings', 'alternate_settings'), ('traceback', None), ('verbosity', '1')]") class StartProject(LiveServerTestCase, AdminScriptTestCase): diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py index 637f643261..8c63e90ce1 100644 --- a/tests/admin_util/tests.py +++ b/tests/admin_util/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime @@ -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">&text:</label>') self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), - '<label for="id_cb" class="vCheckboxLabel required inline">&cb:</label>') + '<label for="id_cb" class="vCheckboxLabel required inline">&cb</label>') def test_flatten_fieldsets(self): """ diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 5eee3e7105..39e74a945c 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -1,9 +1,10 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django import forms from django.contrib import admin from django.core.exceptions import ImproperlyConfigured from django.test import TestCase +from django.test.utils import str_prefix from .models import Song, Book, Album, TwoAlbumFKAndAnE, State, City @@ -185,7 +186,8 @@ class ValidationTestCase(TestCase): readonly_fields = ("title", "nonexistant") self.assertRaisesMessage(ImproperlyConfigured, - "SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.", + str_prefix("SongAdmin.readonly_fields[1], %(_)s'nonexistant' is not a callable " + "or an attribute of 'SongAdmin' or found in the model 'Song'."), SongAdmin.validate, Song) @@ -195,7 +197,8 @@ class ValidationTestCase(TestCase): readonly_fields=['i_dont_exist'] # Missing attribute self.assertRaisesMessage(ImproperlyConfigured, - "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.", + str_prefix("CityInline.readonly_fields[0], %(_)s'i_dont_exist' is not a callable " + "or an attribute of 'CityInline' or found in the model 'City'."), CityInline.validate, City) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index 039abb819b..df8ced949e 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import tempfile import os diff --git a/tests/admin_views/customadmin.py b/tests/admin_views/customadmin.py index c204b81edd..f964d6cffb 100644 --- a/tests/admin_views/customadmin.py +++ b/tests/admin_views/customadmin.py @@ -1,7 +1,7 @@ """ A second, custom AdminSite -- see tests.CustomAdminSiteTests. """ -from __future__ import absolute_import +from __future__ import unicode_literals from django.conf.urls import patterns from django.contrib import admin @@ -13,6 +13,7 @@ from . import models, forms, admin as base_admin class Admin2(admin.AdminSite): + app_index_template = 'custom_admin/app_index.html' login_form = forms.CustomAdminAuthenticationForm login_template = 'custom_admin/login.html' logout_template = 'custom_admin/logout.html' diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 5ec4fbb544..a1131210d7 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -644,8 +644,8 @@ class MainPrepopulated(models.Model): max_length=20, choices=(('option one', 'Option One'), ('option two', 'Option Two'))) - slug1 = models.SlugField() - slug2 = models.SlugField() + slug1 = models.SlugField(blank=True) + slug2 = models.SlugField(blank=True) class RelatedPrepopulated(models.Model): parent = models.ForeignKey(MainPrepopulated) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 7decf6f471..3ee06751fb 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import os import re @@ -806,6 +806,12 @@ class CustomModelAdminTest(AdminViewBasicTestCase): self.assertTemplateUsed(response, 'custom_admin/index.html') self.assertContains(response, 'Hello from a custom index template *bar*') + def testCustomAdminSiteAppIndexViewandTemplate(self): + response = self.client.get('/test_admin/admin2/admin_views/') + self.assertIsInstance(response, TemplateResponse) + self.assertTemplateUsed(response, 'custom_admin/app_index.html') + self.assertContains(response, 'Hello from a custom app_index template') + def testCustomAdminSitePasswordChangeTemplate(self): response = self.client.get('/test_admin/admin2/password_change/') self.assertIsInstance(response, TemplateResponse) @@ -1496,7 +1502,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get(prefix) # this URL now comes through reverse(), thus url quoting and iri_to_uri encoding pk_final_url = escape(iri_to_uri(urlquote(quote(self.pk)))) - should_contain = """<th><a href="%s%s/">%s</a></th>""" % (prefix, pk_final_url, escape(self.pk)) + should_contain = """<th class="field-__str__"><a href="%s%s/">%s</a></th>""" % (prefix, pk_final_url, escape(self.pk)) self.assertContains(response, should_contain) def test_recentactions_link(self): @@ -1527,6 +1533,17 @@ class AdminViewStringPrimaryKeyTest(TestCase): self.assertEqual(counted_presence_before - 1, counted_presence_after) + def test_logentry_get_admin_url(self): + "LogEntry.get_admin_url returns a URL to edit the entry's object or None for non-existent (possibly deleted) models" + log_entry_name = "Model with string primary key" # capitalized in Recent Actions + logentry = LogEntry.objects.get(content_type__name__iexact=log_entry_name) + model = "modelwithstringprimarykey" + desired_admin_url = "/test_admin/admin/admin_views/%s/%s/" % (model, escape(iri_to_uri(urlquote(quote(self.pk))))) + self.assertEqual(logentry.get_admin_url(), desired_admin_url) + + logentry.content_type.model = "non-existent" + self.assertEqual(logentry.get_admin_url(), None) + def test_deleteconfirmation_link(self): "The link from the delete confirmation page referring back to the changeform of the object should be quoted" response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk)) @@ -2151,8 +2168,8 @@ class AdminViewListEditable(TestCase): self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True) - self.assertContains(response, '<td>%d</td>' % story1.id, 1) - self.assertContains(response, '<td>%d</td>' % story2.id, 1) + self.assertContains(response, '<td class="field-id">%d</td>' % story1.id, 1) + self.assertContains(response, '<td class="field-id">%d</td>' % story2.id, 1) def test_pk_hidden_fields_with_list_display_links(self): """ Similarly as test_pk_hidden_fields, but when the hidden pk fields are @@ -2167,8 +2184,8 @@ class AdminViewListEditable(TestCase): self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '<div class="hiddenfields">\n<input type="hidden" name="form-0-id" value="%d" id="id_form-0-id" /><input type="hidden" name="form-1-id" value="%d" id="id_form-1-id" />\n</div>' % (story2.id, story1.id), html=True) - self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link1, story1.id), 1) - self.assertContains(response, '<th><a href="%s">%d</a></th>' % (link2, story2.id), 1) + self.assertContains(response, '<th class="field-id"><a href="%s">%d</a></th>' % (link1, story1.id), 1) + self.assertContains(response, '<th class="field-id"><a href="%s">%d</a></th>' % (link2, story2.id), 1) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -3438,6 +3455,50 @@ class SeleniumAdminViewsFirefoxTests(AdminSeleniumWebDriverTestCase): slug2='option-one-tabular-inline-ignored-characters', ) + def test_populate_existing_object(self): + """ + Ensure that the prepopulation works for existing objects too, as long + as the original field is empty. + Refs #19082. + """ + # Slugs are empty to start with. + item = MainPrepopulated.objects.create( + name=' this is the mAin nÀMë', + pubdate='2012-02-18', + status='option two', + slug1='', + slug2='', + ) + self.admin_login(username='super', + password='secret', + login_url='/test_admin/admin/') + + object_url = '%s%s' % ( + self.live_server_url, + '/test_admin/admin/admin_views/mainprepopulated/{}/'.format(item.id)) + + self.selenium.get(object_url) + self.selenium.find_element_by_css_selector('#id_name').send_keys(' the best') + + # The slugs got prepopulated since they were originally empty + slug1 = self.selenium.find_element_by_css_selector('#id_slug1').get_attribute('value') + slug2 = self.selenium.find_element_by_css_selector('#id_slug2').get_attribute('value') + self.assertEqual(slug1, 'main-name-best-2012-02-18') + self.assertEqual(slug2, 'option-two-main-name-best') + + # Save the object + self.selenium.find_element_by_xpath('//input[@value="Save"]').click() + self.wait_page_loaded() + + self.selenium.get(object_url) + self.selenium.find_element_by_css_selector('#id_name').send_keys(' hello') + + # The slugs got prepopulated didn't change since they were originally not empty + slug1 = self.selenium.find_element_by_css_selector('#id_slug1').get_attribute('value') + slug2 = self.selenium.find_element_by_css_selector('#id_slug2').get_attribute('value') + self.assertEqual(slug1, 'main-name-best-2012-02-18') + self.assertEqual(slug2, 'option-two-main-name-best') + def test_collapsible_fieldset(self): """ Test that the 'collapse' class in fieldsets definition allows to @@ -3877,6 +3938,22 @@ class CSSTest(TestCase): self.assertContains(response, '<body class="app-admin_views model-section ') + def test_changelist_field_classes(self): + """ + Cells of the change list table should contain the field name in their class attribute + Refs #11195. + """ + Podcast.objects.create(name="Django Dose", + release_date=datetime.date.today()) + response = self.client.get('/test_admin/admin/admin_views/podcast/') + self.assertContains( + response, '<th class="field-name">') + self.assertContains( + response, '<td class="field-release_date nowrap">') + self.assertContains( + response, '<td class="action-checkbox">') + + try: import docutils except ImportError: diff --git a/tests/admin_views/urls.py b/tests/admin_views/urls.py index 763c83a450..d934173234 100644 --- a/tests/admin_views/urls.py +++ b/tests/admin_views/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, include from . import views, customadmin, admin diff --git a/tests/admin_widgets/models.py b/tests/admin_widgets/models.py index ae19d58cc4..cb97242e03 100644 --- a/tests/admin_widgets/models.py +++ b/tests/admin_widgets/models.py @@ -13,6 +13,7 @@ class Member(models.Model): name = models.CharField(max_length=100) birthdate = models.DateTimeField(blank=True, null=True) gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) + email = models.EmailField(blank=True) def __str__(self): return self.name @@ -55,7 +56,8 @@ class Inventory(models.Model): return self.name class Event(models.Model): - band = models.ForeignKey(Band, limit_choices_to=models.Q(pk__gt=0)) + main_band = models.ForeignKey(Band, limit_choices_to=models.Q(pk__gt=0), related_name='events_main_band_at') + supporting_bands = models.ManyToManyField(Band, null=True, blank=True, related_name='events_supporting_band_at') start_date = models.DateField(blank=True, null=True) start_time = models.TimeField(blank=True, null=True) description = models.TextField(blank=True) diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 870aa6d455..5a88df1e57 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -1,5 +1,5 @@ # encoding: utf-8 -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime, timedelta from unittest import TestCase @@ -83,19 +83,22 @@ class AdminFormfieldForDBFieldTests(TestCase): def testCharField(self): self.assertFormfield(models.Member, 'name', widgets.AdminTextInputWidget) + def testEmailField(self): + self.assertFormfield(models.Member, 'email', widgets.AdminEmailInputWidget) + def testFileField(self): self.assertFormfield(models.Album, 'cover_art', widgets.AdminFileWidget) def testForeignKey(self): - self.assertFormfield(models.Event, 'band', forms.Select) + self.assertFormfield(models.Event, 'main_band', forms.Select) def testRawIDForeignKey(self): - self.assertFormfield(models.Event, 'band', widgets.ForeignKeyRawIdWidget, - raw_id_fields=['band']) + self.assertFormfield(models.Event, 'main_band', widgets.ForeignKeyRawIdWidget, + raw_id_fields=['main_band']) def testRadioFieldsForeignKey(self): - ff = self.assertFormfield(models.Event, 'band', widgets.AdminRadioSelect, - radio_fields={'band':admin.VERTICAL}) + ff = self.assertFormfield(models.Event, 'main_band', widgets.AdminRadioSelect, + radio_fields={'main_band':admin.VERTICAL}) self.assertEqual(ff.empty_label, None) def testManyToMany(self): @@ -198,7 +201,7 @@ class AdminForeignKeyRawIdWidget(DjangoTestCase): pk = band.pk band.delete() post_data = { - "band": '%s' % pk, + "main_band": '%s' % pk, } # Try posting with a non-existent pk in a raw id field: this # should result in an error message, not a server exception. @@ -212,7 +215,7 @@ class AdminForeignKeyRawIdWidget(DjangoTestCase): for test_str in ('Iñtërnâtiônàlizætiøn', "1234'", -1234): # This should result in an error message, not a server exception. response = self.client.post('%s/admin_widgets/event/add/' % self.admin_root, - {"band": test_str}) + {"main_band": test_str}) self.assertContains(response, 'Select a valid choice. That choice is not one of the available choices.') @@ -223,6 +226,13 @@ class AdminForeignKeyRawIdWidget(DjangoTestCase): self.assertEqual(lookup1, {'color__in': 'red,blue'}) self.assertEqual(lookup1, lookup2) + def test_url_params_from_lookup_dict_callable(self): + def my_callable(): + return 'works' + lookup1 = widgets.url_params_from_lookup_dict({'myfield': my_callable}) + lookup2 = widgets.url_params_from_lookup_dict({'myfield': my_callable()}) + self.assertEqual(lookup1, lookup2) + class FilteredSelectMultipleWidgetTest(DjangoTestCase): def test_render(self): @@ -300,29 +310,29 @@ class AdminURLWidgetTest(DjangoTestCase): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', '')), - '<input class="vURLField" name="test" type="text" />' + '<input class="vURLField" name="test" type="url" />' ) self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example.com')), - '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>' + '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="url" value="http://example.com" /></p>' ) def test_render_idn(self): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example-äüö.com')), - '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>' + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="url" value="http://example-äüö.com" /></p>' ) def test_render_quoting(self): w = widgets.AdminURLFieldWidget() self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')), - '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>' + '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="url" value="http://example.com/<sometag>some text</sometag>" /></p>' ) self.assertHTMLEqual( conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')), - '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>' + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/<sometag>some text</sometag></a><br />Change:<input class="vURLField" name="test" type="url" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>' ) @@ -816,3 +826,100 @@ class HorizontalVerticalFilterSeleniumChromeTests(HorizontalVerticalFilterSeleni class HorizontalVerticalFilterSeleniumIETests(HorizontalVerticalFilterSeleniumFirefoxTests): webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class AdminRawIdWidgetSeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): + 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): + models.Band.objects.create(id=42, name='Bogey Blues') + models.Band.objects.create(id=98, name='Green Potatoes') + super(AdminRawIdWidgetSeleniumFirefoxTests, self).setUp() + + def test_foreignkey(self): + self.admin_login(username='super', password='secret', login_url='/') + self.selenium.get( + '%s%s' % (self.live_server_url, '/admin_widgets/event/add/')) + main_window = self.selenium.current_window_handle + + # No value has been selected yet + self.assertEqual( + self.selenium.find_element_by_id('id_main_band').get_attribute('value'), + '') + + # Open the popup window and click on a band + self.selenium.find_element_by_id('lookup_id_main_band').click() + self.selenium.switch_to_window('id_main_band') + self.wait_page_loaded() + link = self.selenium.find_element_by_link_text('Bogey Blues') + self.assertTrue('/band/42/' in link.get_attribute('href')) + link.click() + + # The field now contains the selected band's id + self.selenium.switch_to_window(main_window) + self.assertEqual( + self.selenium.find_element_by_id('id_main_band').get_attribute('value'), + '42') + + # Reopen the popup window and click on another band + self.selenium.find_element_by_id('lookup_id_main_band').click() + self.selenium.switch_to_window('id_main_band') + self.wait_page_loaded() + link = self.selenium.find_element_by_link_text('Green Potatoes') + self.assertTrue('/band/98/' in link.get_attribute('href')) + link.click() + + # The field now contains the other selected band's id + self.selenium.switch_to_window(main_window) + self.assertEqual( + self.selenium.find_element_by_id('id_main_band').get_attribute('value'), + '98') + + def test_many_to_many(self): + self.admin_login(username='super', password='secret', login_url='/') + self.selenium.get( + '%s%s' % (self.live_server_url, '/admin_widgets/event/add/')) + main_window = self.selenium.current_window_handle + + # No value has been selected yet + self.assertEqual( + self.selenium.find_element_by_id('id_supporting_bands').get_attribute('value'), + '') + + # Open the popup window and click on a band + self.selenium.find_element_by_id('lookup_id_supporting_bands').click() + self.selenium.switch_to_window('id_supporting_bands') + self.wait_page_loaded() + link = self.selenium.find_element_by_link_text('Bogey Blues') + self.assertTrue('/band/42/' in link.get_attribute('href')) + link.click() + + # The field now contains the selected band's id + self.selenium.switch_to_window(main_window) + self.assertEqual( + self.selenium.find_element_by_id('id_supporting_bands').get_attribute('value'), + '42') + + # Reopen the popup window and click on another band + self.selenium.find_element_by_id('lookup_id_supporting_bands').click() + self.selenium.switch_to_window('id_supporting_bands') + self.wait_page_loaded() + link = self.selenium.find_element_by_link_text('Green Potatoes') + self.assertTrue('/band/98/' in link.get_attribute('href')) + link.click() + + # The field now contains the two selected bands' ids + self.selenium.switch_to_window(main_window) + self.assertEqual( + self.selenium.find_element_by_id('id_supporting_bands').get_attribute('value'), + '42,98') + +class AdminRawIdWidgetSeleniumChromeTests(AdminRawIdWidgetSeleniumFirefoxTests): + webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' + +class AdminRawIdWidgetSeleniumIETests(AdminRawIdWidgetSeleniumFirefoxTests): + webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' diff --git a/tests/admin_widgets/urls.py b/tests/admin_widgets/urls.py index aecee90b7f..3da5d25bc8 100644 --- a/tests/admin_widgets/urls.py +++ b/tests/admin_widgets/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, include from . import widgetadmin diff --git a/tests/admin_widgets/widgetadmin.py b/tests/admin_widgets/widgetadmin.py index 1cdeeb9f67..4894f92afb 100644 --- a/tests/admin_widgets/widgetadmin.py +++ b/tests/admin_widgets/widgetadmin.py @@ -1,8 +1,3 @@ -""" - -""" -from __future__ import absolute_import - from django.contrib import admin from . import models @@ -23,7 +18,7 @@ class CarTireAdmin(admin.ModelAdmin): return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) class EventAdmin(admin.ModelAdmin): - raw_id_fields = ['band'] + raw_id_fields = ['main_band', 'supporting_bands'] class SchoolAdmin(admin.ModelAdmin): @@ -47,4 +42,4 @@ site.register(models.Bee) site.register(models.Advisor) -site.register(models.School, SchoolAdmin)
\ No newline at end of file +site.register(models.School, SchoolAdmin) diff --git a/tests/aggregation/tests.py b/tests/aggregation/tests.py index c635e6ebb6..4d46cae766 100644 --- a/tests/aggregation/tests.py +++ b/tests/aggregation/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime from decimal import Decimal @@ -585,3 +585,35 @@ class BaseAggregateTestCase(TestCase): "datetime.date(2008, 1, 1)" ] ) + + def test_values_aggregation(self): + # Refs #20782 + max_rating = Book.objects.values('rating').aggregate(max_rating=Max('rating')) + self.assertEqual(max_rating['max_rating'], 5) + max_books_per_rating = Book.objects.values('rating').annotate( + books_per_rating=Count('id') + ).aggregate(Max('books_per_rating')) + self.assertEqual( + max_books_per_rating, + {'books_per_rating__max': 3}) + + def test_ticket17424(self): + """ + Check that doing exclude() on a foreign model after annotate() + doesn't crash. + """ + all_books = list(Book.objects.values_list('pk', flat=True).order_by('pk')) + annotated_books = Book.objects.order_by('pk').annotate(one=Count("id")) + + # The value doesn't matter, we just need any negative + # constraint on a related model that's a noop. + excluded_books = annotated_books.exclude(publisher__name="__UNLIKELY_VALUE__") + + # Try to generate query tree + str(excluded_books.query) + + self.assertQuerysetEqual(excluded_books, all_books, lambda x: x.pk) + + # Check internal state + self.assertIsNone(annotated_books.query.alias_map["aggregation_book"].join_type) + self.assertIsNone(excluded_books.query.alias_map["aggregation_book"].join_type) diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 35ac57d9be..8388e78dbe 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import pickle @@ -250,6 +250,13 @@ class AggregationTests(TestCase): 'price__max': Decimal("82.80") }) + # Regression for #15624 - Missing SELECT columns when using values, annotate + # and aggregate in a single query + self.assertEqual( + Book.objects.annotate(c=Count('authors')).values('c').aggregate(Max('c')), + {'c__max': 3} + ) + def test_field_error(self): # Bad field requests in aggregates are caught and reported self.assertRaises( diff --git a/tests/app_loading/tests.py b/tests/app_loading/tests.py index e0c7cbd78c..dc25899646 100644 --- a/tests/app_loading/tests.py +++ b/tests/app_loading/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import copy import os @@ -7,7 +7,8 @@ import time from unittest import TestCase from django.conf import Settings -from django.db.models.loading import cache, load_app, get_model, get_models +from django.db.models.loading import cache, load_app, get_model, get_models, AppCache +from django.test.utils import override_settings from django.utils._os import upath @@ -61,12 +62,33 @@ class EggLoadingTest(TestCase): egg_name = '%s/brokenapp.egg' % self.egg_dir sys.path.append(egg_name) self.assertRaises(ImportError, load_app, 'broken_app') + raised = None try: load_app('broken_app') except ImportError as e: - # Make sure the message is indicating the actual - # problem in the broken app. - self.assertTrue("modelz" in e.args[0]) + raised = e + + # Make sure the message is indicating the actual + # problem in the broken app. + self.assertTrue(raised is not None) + self.assertTrue("modelz" in raised.args[0]) + + def test_missing_app(self): + """ + Test that repeated app loading doesn't succeed in case there is an + error. Refs #17667. + """ + # AppCache is a Borg, so we can instantiate one and change its + # loaded to False to force the following code to actually try to + # populate the cache. + a = AppCache() + a.loaded = False + try: + with override_settings(INSTALLED_APPS=('notexists',)): + self.assertRaises(ImportError, get_model, 'notexists', 'nomodel', seed_cache=True) + self.assertRaises(ImportError, get_model, 'notexists', 'nomodel', seed_cache=True) + finally: + a.loaded = True class GetModelsTest(TestCase): diff --git a/tests/backends/models.py b/tests/backends/models.py index 4f03ddeacc..1508af4354 100644 --- a/tests/backends/models.py +++ b/tests/backends/models.py @@ -68,11 +68,18 @@ class Reporter(models.Model): return "%s %s" % (self.first_name, self.last_name) +class ReporterProxy(Reporter): + class Meta: + proxy = True + + @python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateField() reporter = models.ForeignKey(Reporter) + reporter_proxy = models.ForeignKey(ReporterProxy, null=True, + related_name='reporter_proxy') def __str__(self): return self.headline diff --git a/tests/backends/tests.py b/tests/backends/tests.py index ec592f0188..d6e3f5c78e 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Unit and doctests for specific database backends. -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime from decimal import Decimal @@ -614,12 +614,19 @@ class FkConstraintsTests(TransactionTestCase): Try to create a model instance that violates a FK constraint. If it fails it should fail with IntegrityError. """ - a = models.Article(headline="This is a test", pub_date=datetime.datetime(2005, 7, 27), reporter_id=30) + a1 = models.Article(headline="This is a test", pub_date=datetime.datetime(2005, 7, 27), reporter_id=30) try: - a.save() + a1.save() except IntegrityError: - return - self.skipTest("This backend does not support integrity checks.") + pass + else: + self.skipTest("This backend does not support integrity checks.") + # Now that we know this backend supports integrity checks we make sure + # constraints are also enforced for proxy models. Refs #17519 + a2 = models.Article(headline='This is another test', reporter=self.r, + pub_date=datetime.datetime(2012, 8, 3), + reporter_proxy_id=30) + self.assertRaises(IntegrityError, a2.save) def test_integrity_checks_on_update(self): """ @@ -628,14 +635,26 @@ class FkConstraintsTests(TransactionTestCase): """ # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) - # Retrive it from the DB - a = models.Article.objects.get(headline="Test article") - a.reporter_id = 30 + # Retrieve it from the DB + a1 = models.Article.objects.get(headline="Test article") + a1.reporter_id = 30 try: - a.save() + a1.save() except IntegrityError: - return - self.skipTest("This backend does not support integrity checks.") + pass + else: + self.skipTest("This backend does not support integrity checks.") + # Now that we know this backend supports integrity checks we make sure + # constraints are also enforced for proxy models. Refs #17519 + # Create another article + r_proxy = models.ReporterProxy.objects.get(pk=self.r.pk) + models.Article.objects.create(headline='Another article', + pub_date=datetime.datetime(1988, 5, 15), + reporter=self.r, reporter_proxy=r_proxy) + # Retreive the second article from the DB + a2 = models.Article.objects.get(headline='Another article') + a2.reporter_proxy_id = 30 + self.assertRaises(IntegrityError, a2.save) def test_disable_constraint_checks_manually(self): """ diff --git a/tests/basic/tests.py b/tests/basic/tests.py index fb21b11279..55ed6a436f 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime import threading @@ -6,6 +6,7 @@ import threading from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import connections, DEFAULT_DB_ALIAS from django.db.models.fields import Field, FieldDoesNotExist +from django.db.models.manager import BaseManager from django.db.models.query import QuerySet, EmptyQuerySet, ValuesListQuerySet, MAX_GET_RESULTS from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import six @@ -734,3 +735,59 @@ class ConcurrentSaveTests(TransactionTestCase): t.join() a.save() self.assertEqual(Article.objects.get(pk=a.pk).headline, 'foo') + + +class ManagerTest(TestCase): + QUERYSET_PROXY_METHODS = [ + 'none', + 'count', + 'dates', + 'datetimes', + 'distinct', + 'extra', + 'get', + 'get_or_create', + 'update_or_create', + 'create', + 'bulk_create', + 'filter', + 'aggregate', + 'annotate', + 'complex_filter', + 'exclude', + 'in_bulk', + 'iterator', + 'earliest', + 'latest', + 'first', + 'last', + 'order_by', + 'select_for_update', + 'select_related', + 'prefetch_related', + 'values', + 'values_list', + 'update', + 'reverse', + 'defer', + 'only', + 'using', + 'exists', + '_insert', + '_update', + 'raw', + ] + + def test_manager_methods(self): + """ + This test ensures that the correct set of methods from `QuerySet` + are copied onto `Manager`. + + It's particularly useful to prevent accidentally leaking new methods + into `Manager`. New `QuerySet` methods that should also be copied onto + `Manager` will need to be added to `ManagerTest.QUERYSET_PROXY_METHODS`. + """ + self.assertEqual( + sorted(BaseManager._get_queryset_methods(QuerySet).keys()), + sorted(self.QUERYSET_PROXY_METHODS), + ) diff --git a/tests/bug639/tests.py b/tests/bug639/tests.py index 71c50e14aa..769d27d2f0 100644 --- a/tests/bug639/tests.py +++ b/tests/bug639/tests.py @@ -4,8 +4,6 @@ gets called *again* for each FileField. This test will fail if calling a ModelForm's save() method causes Model.save() to be called more than once. """ -from __future__ import absolute_import - import os import shutil import unittest diff --git a/tests/bug8245/admin.py b/tests/bug8245/admin.py index d821190763..e7d1a080c2 100644 --- a/tests/bug8245/admin.py +++ b/tests/bug8245/admin.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from .models import Story diff --git a/tests/bulk_create/tests.py b/tests/bulk_create/tests.py index d4772934a1..94258ca711 100644 --- a/tests/bulk_create/tests.py +++ b/tests/bulk_create/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 5cf199794c..287a078617 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -2,7 +2,7 @@ # Unit tests for cache framework # Uses whatever cache backend is set in the test settings file. -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import hashlib import os diff --git a/tests/choices/tests.py b/tests/choices/tests.py index 03a7d3340d..2d612626c2 100644 --- a/tests/choices/tests.py +++ b/tests/choices/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.test import TestCase from .models import Person diff --git a/tests/comment_tests/tests/__init__.py b/tests/comment_tests/tests/__init__.py index ae4585e187..6cbddbe82b 100644 --- a/tests/comment_tests/tests/__init__.py +++ b/tests/comment_tests/tests/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.auth.models import User from django.contrib.comments.forms import CommentForm from django.contrib.comments.models import Comment diff --git a/tests/comment_tests/tests/test_app_api.py b/tests/comment_tests/tests/test_app_api.py index ed23ba39cc..83ee868a02 100644 --- a/tests/comment_tests/tests/test_app_api.py +++ b/tests/comment_tests/tests/test_app_api.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf import settings from django.contrib import comments from django.contrib.comments.models import Comment diff --git a/tests/comment_tests/tests/test_comment_form.py b/tests/comment_tests/tests/test_comment_form.py index a30f13a073..bca339fd3d 100644 --- a/tests/comment_tests/tests/test_comment_form.py +++ b/tests/comment_tests/tests/test_comment_form.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import time from django.conf import settings diff --git a/tests/comment_tests/tests/test_comment_utils_moderators.py b/tests/comment_tests/tests/test_comment_utils_moderators.py index 6c7a882e25..e61be48d8b 100644 --- a/tests/comment_tests/tests/test_comment_utils_moderators.py +++ b/tests/comment_tests/tests/test_comment_utils_moderators.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.comments.models import Comment from django.contrib.comments.moderation import (moderator, CommentModerator, AlreadyModerated) diff --git a/tests/comment_tests/tests/test_comment_view.py b/tests/comment_tests/tests/test_comment_view.py index 0d994d3af8..19d7c1d16b 100644 --- a/tests/comment_tests/tests/test_comment_view.py +++ b/tests/comment_tests/tests/test_comment_view.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import re diff --git a/tests/comment_tests/tests/test_feeds.py b/tests/comment_tests/tests/test_feeds.py index 941ffb6bf2..e6652afa28 100644 --- a/tests/comment_tests/tests/test_feeds.py +++ b/tests/comment_tests/tests/test_feeds.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from xml.etree import ElementTree as ET from django.conf import settings diff --git a/tests/comment_tests/tests/test_models.py b/tests/comment_tests/tests/test_models.py index 69c1a8118f..4303fda852 100644 --- a/tests/comment_tests/tests/test_models.py +++ b/tests/comment_tests/tests/test_models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.comments.models import Comment from . import CommentTestCase diff --git a/tests/comment_tests/tests/test_moderation_views.py b/tests/comment_tests/tests/test_moderation_views.py index 02af35cfe4..9f826f5866 100644 --- a/tests/comment_tests/tests/test_moderation_views.py +++ b/tests/comment_tests/tests/test_moderation_views.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.auth.models import User, Permission from django.contrib.comments import signals diff --git a/tests/comment_tests/tests/test_templatetags.py b/tests/comment_tests/tests/test_templatetags.py index 1971c21a58..d24859eb55 100644 --- a/tests/comment_tests/tests/test_templatetags.py +++ b/tests/comment_tests/tests/test_templatetags.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.comments.forms import CommentForm from django.contrib.comments.models import Comment from django.contrib.contenttypes.models import ContentType diff --git a/tests/comment_tests/urls.py b/tests/comment_tests/urls.py index 0a7e8b5fdf..32106106b5 100644 --- a/tests/comment_tests/urls.py +++ b/tests/comment_tests/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from django.contrib.comments.feeds import LatestCommentFeed diff --git a/tests/conditional_processing/views.py b/tests/conditional_processing/views.py index 14a5a83a45..d4dc4b8cf6 100644 --- a/tests/conditional_processing/views.py +++ b/tests/conditional_processing/views.py @@ -1,6 +1,3 @@ -# -*- coding:utf-8 -*- -from __future__ import absolute_import - from django.views.decorators.http import condition, etag, last_modified from django.http import HttpResponse diff --git a/tests/contenttypes_tests/models.py b/tests/contenttypes_tests/models.py index 3c6685687a..5d21ad5b96 100644 --- a/tests/contenttypes_tests/models.py +++ b/tests/contenttypes_tests/models.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.db import models from django.utils.encoding import python_2_unicode_compatible diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py index b39e118ec6..63f02697df 100644 --- a/tests/contenttypes_tests/tests.py +++ b/tests/contenttypes_tests/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType from django.test import TestCase diff --git a/tests/contenttypes_tests/urls.py b/tests/contenttypes_tests/urls.py index 2cfc90b024..626b3ae8f7 100644 --- a/tests/contenttypes_tests/urls.py +++ b/tests/contenttypes_tests/urls.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf.urls import patterns diff --git a/tests/context_processors/urls.py b/tests/context_processors/urls.py index 757017c9ca..9340cdfc38 100644 --- a/tests/context_processors/urls.py +++ b/tests/context_processors/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from . import views diff --git a/tests/custom_columns/tests.py b/tests/custom_columns/tests.py index 51028b0c46..155279297f 100644 --- a/tests/custom_columns/tests.py +++ b/tests/custom_columns/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import FieldError from django.test import TestCase diff --git a/tests/custom_columns_regress/tests.py b/tests/custom_columns_regress/tests.py index 7cc66ca2a6..034de08d45 100644 --- a/tests/custom_columns_regress/tests.py +++ b/tests/custom_columns_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import FieldError from django.test import TestCase diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index 2f5e62fc7a..44d5eb70da 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -20,6 +20,49 @@ class PersonManager(models.Manager): def get_fun_people(self): return self.filter(fun=True) +# An example of a custom manager that sets get_queryset(). + +class PublishedBookManager(models.Manager): + def get_queryset(self): + return super(PublishedBookManager, self).get_queryset().filter(is_published=True) + +# An example of a custom queryset that copies its methods onto the manager. + +class CustomQuerySet(models.QuerySet): + def filter(self, *args, **kwargs): + queryset = super(CustomQuerySet, self).filter(fun=True) + queryset._filter_CustomQuerySet = True + return queryset + + def public_method(self, *args, **kwargs): + return self.all() + + def _private_method(self, *args, **kwargs): + return self.all() + + def optout_public_method(self, *args, **kwargs): + return self.all() + optout_public_method.queryset_only = True + + def _optin_private_method(self, *args, **kwargs): + return self.all() + _optin_private_method.queryset_only = False + +class BaseCustomManager(models.Manager): + def __init__(self, arg): + super(BaseCustomManager, self).__init__() + self.init_arg = arg + + def filter(self, *args, **kwargs): + queryset = super(BaseCustomManager, self).filter(fun=True) + queryset._filter_CustomManager = True + return queryset + + def manager_only(self): + return self.all() + +CustomManager = BaseCustomManager.from_queryset(CustomQuerySet) + @python_2_unicode_compatible class Person(models.Model): first_name = models.CharField(max_length=30) @@ -27,15 +70,12 @@ class Person(models.Model): fun = models.BooleanField() objects = PersonManager() + custom_queryset_default_manager = CustomQuerySet.as_manager() + custom_queryset_custom_manager = CustomManager('hello') + def __str__(self): return "%s %s" % (self.first_name, self.last_name) -# An example of a custom manager that sets get_queryset(). - -class PublishedBookManager(models.Manager): - def get_queryset(self): - return super(PublishedBookManager, self).get_queryset().filter(is_published=True) - @python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 4fe79fe3fb..87e5a721bc 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase from django.utils import six @@ -11,12 +11,54 @@ class CustomManagerTests(TestCase): p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ "Bugs Bunny" ], six.text_type ) + + # Test that the methods of a custom `QuerySet` are properly + # copied onto the default `Manager`. + for manager in ['custom_queryset_default_manager', + 'custom_queryset_custom_manager']: + manager = getattr(Person, manager) + + # Copy public methods. + manager.public_method() + # Don't copy private methods. + with self.assertRaises(AttributeError): + manager._private_method() + # Copy methods with `manager=True` even if they are private. + manager._optin_private_method() + # Don't copy methods with `manager=False` even if they are public. + with self.assertRaises(AttributeError): + manager.optout_public_method() + + # Test that the overridden method is called. + queryset = manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that specialized querysets inherit from our custom queryset. + queryset = manager.values_list('first_name', flat=True).filter() + self.assertEqual(list(queryset), [six.text_type("Bugs")]) + self.assertEqual(queryset._filter_CustomQuerySet, True) + + # Test that the custom manager `__init__()` argument has been set. + self.assertEqual(Person.custom_queryset_custom_manager.init_arg, 'hello') + + # Test that the custom manager method is only available on the manager. + Person.custom_queryset_custom_manager.manager_only() + with self.assertRaises(AttributeError): + Person.custom_queryset_custom_manager.all().manager_only() + + # Test that the queryset method doesn't override the custom manager method. + queryset = Person.custom_queryset_custom_manager.filter() + self.assertQuerysetEqual(queryset, ["Bugs Bunny"], six.text_type) + self.assertEqual(queryset._filter_CustomManager, True) + # The RelatedManager used on the 'books' descriptor extends the default # manager self.assertIsInstance(p2.books, PublishedBookManager) diff --git a/tests/custom_managers_regress/tests.py b/tests/custom_managers_regress/tests.py index 5a9cd91d59..8462e526c9 100644 --- a/tests/custom_managers_regress/tests.py +++ b/tests/custom_managers_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/custom_methods/tests.py b/tests/custom_methods/tests.py index 9d7444ba62..addb61d4c1 100644 --- a/tests/custom_methods/tests.py +++ b/tests/custom_methods/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import date diff --git a/tests/custom_pk/models.py b/tests/custom_pk/models.py index 5ef9b69f0c..8d5065aed8 100644 --- a/tests/custom_pk/models.py +++ b/tests/custom_pk/models.py @@ -6,7 +6,7 @@ By default, Django adds an ``"id"`` field to each model. But you can override this behavior by explicitly adding ``primary_key=True`` to a field. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.db import models diff --git a/tests/custom_pk/tests.py b/tests/custom_pk/tests.py index 3f562f0bed..59bc64754f 100644 --- a/tests/custom_pk/tests.py +++ b/tests/custom_pk/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.db import transaction, IntegrityError from django.test import TestCase, skipIfDBFeature diff --git a/tests/datatypes/tests.py b/tests/datatypes/tests.py index b6b52dedf2..a2bb0c110a 100644 --- a/tests/datatypes/tests.py +++ b/tests/datatypes/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime diff --git a/tests/dates/tests.py b/tests/dates/tests.py index 6c02d597de..c2a8d82e37 100644 --- a/tests/dates/tests.py +++ b/tests/dates/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py index 58cb060f6b..f54b30d967 100644 --- a/tests/datetimes/tests.py +++ b/tests/datetimes/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime diff --git a/tests/defaultfilters/tests.py b/tests/defaultfilters/tests.py index 15b96be881..725b673903 100644 --- a/tests/defaultfilters/tests.py +++ b/tests/defaultfilters/tests.py @@ -249,9 +249,10 @@ class DefaultFiltersTests(TestCase): '<a href="https://google.com" rel="nofollow">https://google.com</a>') # Check urlize doesn't overquote already quoted urls - see #9655 - self.assertEqual(urlize('http://hi.baidu.com/%D6%D8%D0%C2%BF'), - '<a href="http://hi.baidu.com/%D6%D8%D0%C2%BF" rel="nofollow">' - 'http://hi.baidu.com/%D6%D8%D0%C2%BF</a>') + # The teststring is the urlquoted version of 'http://hi.baidu.com/重新开始' + self.assertEqual(urlize('http://hi.baidu.com/%E9%87%8D%E6%96%B0%E5%BC%80%E5%A7%8B'), + '<a href="http://hi.baidu.com/%E9%87%8D%E6%96%B0%E5%BC%80%E5%A7%8B" rel="nofollow">' + 'http://hi.baidu.com/%E9%87%8D%E6%96%B0%E5%BC%80%E5%A7%8B</a>') self.assertEqual(urlize('www.mystore.com/30%OffCoupons!'), '<a href="http://www.mystore.com/30%25OffCoupons!" rel="nofollow">' 'www.mystore.com/30%OffCoupons!</a>') diff --git a/tests/defer/tests.py b/tests/defer/tests.py index 50db5a76b4..1df312e1bf 100644 --- a/tests/defer/tests.py +++ b/tests/defer/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db.models.query_utils import DeferredAttribute, InvalidQuery from django.test import TestCase diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index ad2546794c..619f65163c 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/delete/tests.py b/tests/delete/tests.py index 66173078a0..8fb16f5a2d 100644 --- a/tests/delete/tests.py +++ b/tests/delete/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import models, IntegrityError, connection from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index a4908b2121..22e15baf8e 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime diff --git a/tests/dispatch/tests/__init__.py b/tests/dispatch/tests/__init__.py index b6d26217e1..4c4d4fea8a 100644 --- a/tests/dispatch/tests/__init__.py +++ b/tests/dispatch/tests/__init__.py @@ -2,7 +2,5 @@ Unit-tests for the dispatch project """ -from __future__ import absolute_import - from .test_dispatcher import DispatcherTests, ReceiverTestCase from .test_saferef import SaferefTests diff --git a/tests/distinct_on_fields/tests.py b/tests/distinct_on_fields/tests.py index f62a32e58d..7bc06f0da2 100644 --- a/tests/distinct_on_fields/tests.py +++ b/tests/distinct_on_fields/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db.models import Max from django.test import TestCase, skipUnlessDBFeature diff --git a/tests/empty/tests.py b/tests/empty/tests.py index 6dd9f7c75d..007d04c363 100644 --- a/tests/empty/tests.py +++ b/tests/empty/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.core.exceptions import ImproperlyConfigured from django.db.models.loading import get_app from django.test import TestCase diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py index a351496442..9801d0acbb 100644 --- a/tests/expressions/tests.py +++ b/tests/expressions/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.core.exceptions import FieldError from django.db.models import F diff --git a/tests/expressions_regress/tests.py b/tests/expressions_regress/tests.py index ddb2c83b4f..c4f8085804 100644 --- a/tests/expressions_regress/tests.py +++ b/tests/expressions_regress/tests.py @@ -1,7 +1,7 @@ """ Spanning tests for all the operations that F() expressions can perform. """ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime diff --git a/tests/extra_regress/tests.py b/tests/extra_regress/tests.py index 194b250c99..ae8f1b4423 100644 --- a/tests/extra_regress/tests.py +++ b/tests/extra_regress/tests.py @@ -1,10 +1,10 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals +from collections import OrderedDict import datetime from django.contrib.auth.models import User from django.test import TestCase -from django.utils.datastructures import SortedDict from .models import TestObject, Order, RevisionableModel @@ -74,7 +74,7 @@ class ExtraRegressTests(TestCase): # select portions. Applies when portions are updated or otherwise # moved around. qs = User.objects.extra( - select=SortedDict((("alpha", "%s"), ("beta", "2"), ("gamma", "%s"))), + select=OrderedDict((("alpha", "%s"), ("beta", "2"), ("gamma", "%s"))), select_params=(1, 3) ) qs = qs.extra(select={"beta": 4}) @@ -180,100 +180,100 @@ class ExtraRegressTests(TestCase): obj.save() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values()), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values()), [{'bar': 'second', 'third': 'third', 'second': 'second', 'whiz': 'third', 'foo': 'first', 'id': obj.pk, 'first': 'first'}] ) # Extra clauses after an empty values clause are still included self.assertEqual( - list(TestObject.objects.values().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values().extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [{'bar': 'second', 'third': 'third', 'second': 'second', 'whiz': 'third', 'foo': 'first', 'id': obj.pk, 'first': 'first'}] ) # Extra columns are ignored if not mentioned in the values() clause self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second')), [{'second': 'second', 'first': 'first'}] ) # Extra columns after a non-empty values() clause are ignored self.assertEqual( - list(TestObject.objects.values('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values('first', 'second').extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [{'second': 'second', 'first': 'first'}] ) # Extra columns can be partially returned self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second', 'foo')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('first', 'second', 'foo')), [{'second': 'second', 'foo': 'first', 'first': 'first'}] ) # Also works if only extra columns are included self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('foo', 'whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values('foo', 'whiz')), [{'foo': 'first', 'whiz': 'third'}] ) # Values list works the same way # All columns are returned for an empty values_list() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list()), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list()), [('first', 'second', 'third', obj.pk, 'first', 'second', 'third')] ) # Extra columns after an empty values_list() are still included self.assertEqual( - list(TestObject.objects.values_list().extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values_list().extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [('first', 'second', 'third', obj.pk, 'first', 'second', 'third')] ) # Extra columns ignored completely if not mentioned in values_list() self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second')), [('first', 'second')] ) # Extra columns after a non-empty values_list() clause are ignored completely self.assertEqual( - list(TestObject.objects.values_list('first', 'second').extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), + list(TestObject.objects.values_list('first', 'second').extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third'))))), [('first', 'second')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('second', flat=True)), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('second', flat=True)), ['second'] ) # Only the extra columns specified in the values_list() are returned self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second', 'whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first', 'second', 'whiz')), [('first', 'second', 'third')] ) # ...also works if only extra columns are included self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('foo','whiz')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('foo','whiz')), [('first', 'third')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', flat=True)), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', flat=True)), ['third'] ) # ... and values are returned in the order they are specified self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz','foo')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz','foo')), [('third', 'first')] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first','id')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('first','id')), [('first', obj.pk)] ) self.assertEqual( - list(TestObject.objects.extra(select=SortedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', 'first', 'bar', 'id')), + list(TestObject.objects.extra(select=OrderedDict((('foo','first'), ('bar','second'), ('whiz','third')))).values_list('whiz', 'first', 'bar', 'id')), [('third', 'first', 'second', obj.pk)] ) diff --git a/tests/field_defaults/tests.py b/tests/field_defaults/tests.py index 69dabb5168..d9f28d8c5c 100644 --- a/tests/field_defaults/tests.py +++ b/tests/field_defaults/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from datetime import datetime from django.test import TestCase diff --git a/tests/field_subclassing/models.py b/tests/field_subclassing/models.py index 642573cc83..67a95b02f2 100644 --- a/tests/field_subclassing/models.py +++ b/tests/field_subclassing/models.py @@ -2,8 +2,6 @@ Tests for field subclassing. """ -from __future__ import absolute_import - from django.db import models from django.utils.encoding import force_text diff --git a/tests/field_subclassing/tests.py b/tests/field_subclassing/tests.py index 4945cff1bf..d3b4d9e527 100644 --- a/tests/field_subclassing/tests.py +++ b/tests/field_subclassing/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core import serializers from django.test import TestCase diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index 8cf4c33091..cdd9720374 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import errno import os diff --git a/tests/file_uploads/tests.py b/tests/file_uploads/tests.py index f5a9c10be4..b0f00236d2 100644 --- a/tests/file_uploads/tests.py +++ b/tests/file_uploads/tests.py @@ -1,5 +1,5 @@ #! -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import base64 import errno diff --git a/tests/file_uploads/urls.py b/tests/file_uploads/urls.py index 96efaaa5d8..4782a0dab9 100644 --- a/tests/file_uploads/urls.py +++ b/tests/file_uploads/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns from . import views diff --git a/tests/file_uploads/views.py b/tests/file_uploads/views.py index d1bd2cf44e..8d20a9cb6e 100644 --- a/tests/file_uploads/views.py +++ b/tests/file_uploads/views.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import hashlib import json diff --git a/tests/files/tests.py b/tests/files/tests.py index 54eeee13e4..b353c1a213 100644 --- a/tests/files/tests.py +++ b/tests/files/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import os import gzip diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index 6f2218b19e..78653ffe5e 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import warnings diff --git a/tests/fixtures_model_package/models/sql/book.sql b/tests/fixtures_model_package/models/sql/book.sql new file mode 100644 index 0000000000..9b3918f4d7 --- /dev/null +++ b/tests/fixtures_model_package/models/sql/book.sql @@ -0,0 +1,2 @@ +-- Deprecated search path for custom SQL -- remove in Django 1.9 +INSERT INTO fixtures_model_package_book (name) VALUES ('My Deprecated Book'); diff --git a/tests/fixtures_model_package/sql/book.sql b/tests/fixtures_model_package/sql/book.sql new file mode 100644 index 0000000000..21b1d9465b --- /dev/null +++ b/tests/fixtures_model_package/sql/book.sql @@ -0,0 +1 @@ +INSERT INTO fixtures_model_package_book (name) VALUES ('My Book'); diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index ad82267da3..1e22ac9833 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -5,6 +5,7 @@ import warnings from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase +from django.utils.six import StringIO from .models import Article, Book @@ -110,3 +111,19 @@ class FixtureTestCase(TestCase): ], lambda a: a.headline, ) + + +class InitialSQLTests(TestCase): + + def test_custom_sql(self): + """ + #14300 -- Verify that custom_sql_for_model searches `app/sql` and not + `app/models/sql` (the old location will work until Django 1.9) + """ + out = StringIO() + management.call_command("sqlcustom", "fixtures_model_package", stdout=out) + output = out.getvalue() + self.assertTrue("INSERT INTO fixtures_model_package_book (name) " + "VALUES ('My Book')" in output) + # value from deprecated search path models/sql (remove in Django 1.9) + self.assertTrue("Deprecated Book" in output) diff --git a/tests/fixtures_regress/models.py b/tests/fixtures_regress/models.py index 1cc30d2e51..99096728a7 100644 --- a/tests/fixtures_regress/models.py +++ b/tests/fixtures_regress/models.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.auth.models import User from django.db import models diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index a6bad5716d..f917b21642 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Unittests for fixtures. -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import os import re @@ -181,55 +181,68 @@ class TestFixtures(TestCase): """ Test for ticket #4371 -- Loading a fixture file with invalid data using explicit filename. - Validate that error conditions are caught correctly + Test for ticket #18213 -- warning conditions are caught correctly """ - with six.assertRaisesRegex(self, management.CommandError, - "No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)"): + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") management.call_command( 'loaddata', 'bad_fixture2.xml', verbosity=0, ) + warning = warning_list.pop() + self.assertEqual(warning.category, RuntimeWarning) + self.assertEqual(str(warning.message), "No fixture data found for 'bad_fixture2'. (File format may be invalid.)") def test_invalid_data_no_ext(self): """ Test for ticket #4371 -- Loading a fixture file with invalid data without file extension. - Validate that error conditions are caught correctly + Test for ticket #18213 -- warning conditions are caught correctly """ - with six.assertRaisesRegex(self, management.CommandError, - "No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)"): + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") management.call_command( 'loaddata', 'bad_fixture2', verbosity=0, ) + warning = warning_list.pop() + self.assertEqual(warning.category, RuntimeWarning) + self.assertEqual(str(warning.message), "No fixture data found for 'bad_fixture2'. (File format may be invalid.)") def test_empty(self): """ - Test for ticket #4371 -- Loading a fixture file with no data returns an error. - Validate that error conditions are caught correctly + Test for ticket #18213 -- Loading a fixture file with no data output a warning. + Previously empty fixture raises an error exception, see ticket #4371. """ - with six.assertRaisesRegex(self, management.CommandError, - "No fixture data found for 'empty'. \(File format may be invalid.\)"): + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") management.call_command( 'loaddata', 'empty', verbosity=0, ) + warning = warning_list.pop() + self.assertEqual(warning.category, RuntimeWarning) + self.assertEqual(str(warning.message), "No fixture data found for 'empty'. (File format may be invalid.)") def test_error_message(self): """ - (Regression for #9011 - error message is correct) + Regression for #9011 - error message is correct. + Change from error to warning for ticket #18213. """ - with six.assertRaisesRegex(self, management.CommandError, - "^No fixture data found for 'bad_fixture2'. \(File format may be invalid.\)$"): + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") management.call_command( 'loaddata', 'bad_fixture2', 'animal', verbosity=0, ) + warning = warning_list.pop() + self.assertEqual(warning.category, RuntimeWarning) + self.assertEqual(str(warning.message), "No fixture data found for 'bad_fixture2'. (File format may be invalid.)") def test_pg_sequence_resetting_checks(self): """ diff --git a/tests/force_insert_update/tests.py b/tests/force_insert_update/tests.py index a5b2dcebb5..706a099872 100644 --- a/tests/force_insert_update/tests.py +++ b/tests/force_insert_update/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import transaction, IntegrityError, DatabaseError from django.test import TestCase diff --git a/tests/foreign_object/models.py b/tests/foreign_object/models.py index eee8091a15..d8c3bc10d4 100644 --- a/tests/foreign_object/models.py +++ b/tests/foreign_object/models.py @@ -5,14 +5,15 @@ from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import get_language +@python_2_unicode_compatible class Country(models.Model): # Table Column Fields name = models.CharField(max_length=50) - def __unicode__(self): + def __str__(self): return self.name - +@python_2_unicode_compatible class Person(models.Model): # Table Column Fields name = models.CharField(max_length=128) @@ -26,9 +27,10 @@ class Person(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Group(models.Model): # Table Column Fields name = models.CharField(max_length=128) @@ -38,10 +40,11 @@ class Group(models.Model): class Meta: ordering = ('name',) - def __unicode__(self): + def __str__(self): return self.name +@python_2_unicode_compatible class Membership(models.Model): # Table Column Fields membership_country = models.ForeignKey(Country) @@ -51,17 +54,19 @@ class Membership(models.Model): group_id = models.IntegerField() # Relation Fields - person = models.ForeignObject(Person, + person = models.ForeignObject( + Person, from_fields=['membership_country', 'person_id'], to_fields=['person_country_id', 'id']) - group = models.ForeignObject(Group, + group = models.ForeignObject( + Group, from_fields=['membership_country', 'group_id'], to_fields=['group_country', 'id']) class Meta: ordering = ('date_joined', 'invite_reason') - def __unicode__(self): + def __str__(self): return "%s is a member of %s" % (self.person.name, self.group.name) @@ -73,17 +78,20 @@ class Friendship(models.Model): to_friend_id = models.IntegerField() # Relation Fields - from_friend = models.ForeignObject(Person, + from_friend = models.ForeignObject( + Person, from_fields=['from_friend_country', 'from_friend_id'], to_fields=['person_country_id', 'id'], related_name='from_friend') - to_friend_country = models.ForeignObject(Country, + to_friend_country = models.ForeignObject( + Country, from_fields=['to_friend_country_id'], to_fields=['id'], related_name='to_friend_country') - to_friend = models.ForeignObject(Person, + to_friend = models.ForeignObject( + Person, from_fields=['to_friend_country_id', 'to_friend_id'], to_fields=['person_country_id', 'id'], related_name='to_friend') @@ -140,6 +148,9 @@ class Article(models.Model): except ArticleTranslation.DoesNotExist: return '[No translation found]' +class NewsArticle(Article): + pass + class ArticleTranslation(models.Model): article = models.ForeignKey(Article) lang = models.CharField(max_length='2') @@ -156,5 +167,6 @@ class ArticleTag(models.Model): name = models.CharField(max_length=255) class ArticleIdea(models.Model): - articles = models.ManyToManyField(Article, related_name="ideas", related_query_name="idea_things") + articles = models.ManyToManyField(Article, related_name="ideas", + related_query_name="idea_things") name = models.CharField(max_length=255) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 670fc94dc5..cd81cc68a2 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -1,12 +1,17 @@ import datetime from operator import attrgetter -from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation, ArticleTag, ArticleIdea +from .models import ( + Country, Person, Group, Membership, Friendship, Article, + ArticleTranslation, ArticleTag, ArticleIdea, NewsArticle) from django.test import TestCase from django.utils.translation import activate from django.core.exceptions import FieldError from django import forms +# Note that these tests are testing internal implementation details. +# ForeignObject is not part of public API. + class MultiColumnFKTests(TestCase): def setUp(self): # Creating countries @@ -140,9 +145,9 @@ class MultiColumnFKTests(TestCase): Membership.objects.create(membership_country=self.usa, person=self.jim, group=self.democrat) with self.assertNumQueries(1): - people = [m.person for m in Membership.objects.select_related('person')] + people = [m.person for m in Membership.objects.select_related('person').order_by('pk')] - normal_people = [m.person for m in Membership.objects.all()] + normal_people = [m.person for m in Membership.objects.all().order_by('pk')] self.assertEqual(people, normal_people) def test_prefetch_foreignkey_forward_works(self): @@ -150,19 +155,22 @@ class MultiColumnFKTests(TestCase): Membership.objects.create(membership_country=self.usa, person=self.jim, group=self.democrat) with self.assertNumQueries(2): - people = [m.person for m in Membership.objects.prefetch_related('person')] + people = [ + m.person for m in Membership.objects.prefetch_related('person').order_by('pk')] - normal_people = [m.person for m in Membership.objects.all()] + normal_people = [m.person for m in Membership.objects.order_by('pk')] self.assertEqual(people, normal_people) def test_prefetch_foreignkey_reverse_works(self): Membership.objects.create(membership_country=self.usa, person=self.bob, group=self.cia) Membership.objects.create(membership_country=self.usa, person=self.jim, group=self.democrat) with self.assertNumQueries(2): - membership_sets = [list(p.membership_set.all()) - for p in Person.objects.prefetch_related('membership_set')] + membership_sets = [ + list(p.membership_set.all()) + for p in Person.objects.prefetch_related('membership_set').order_by('pk')] - normal_membership_sets = [list(p.membership_set.all()) for p in Person.objects.all()] + normal_membership_sets = [list(p.membership_set.all()) + for p in Person.objects.order_by('pk')] self.assertEqual(membership_sets, normal_membership_sets) def test_m2m_through_forward_returns_valid_members(self): @@ -339,6 +347,20 @@ class MultiColumnFKTests(TestCase): with self.assertRaises(FieldError): Article.objects.filter(ideas__name="idea1") + def test_inheritance(self): + activate("fi") + na = NewsArticle.objects.create(pub_date=datetime.date.today()) + ArticleTranslation.objects.create( + article=na, lang="fi", title="foo", body="bar") + self.assertQuerysetEqual( + NewsArticle.objects.select_related('active_translation'), + [na], lambda x: x + ) + with self.assertNumQueries(1): + self.assertEqual( + NewsArticle.objects.select_related( + 'active_translation')[0].active_translation.title, + "foo") class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs diff --git a/tests/forms_tests/models.py b/tests/forms_tests/models.py index bec31d12d7..33898ffbb8 100644 --- a/tests/forms_tests/models.py +++ b/tests/forms_tests/models.py @@ -34,7 +34,30 @@ class Defaults(models.Model): class ChoiceModel(models.Model): """For ModelChoiceField and ModelMultipleChoiceField tests.""" + CHOICES = [ + ('', 'No Preference'), + ('f', 'Foo'), + ('b', 'Bar'), + ] + + INTEGER_CHOICES = [ + (None, 'No Preference'), + (1, 'Foo'), + (2, 'Bar'), + ] + + STRING_CHOICES_WITH_NONE = [ + (None, 'No Preference'), + ('f', 'Foo'), + ('b', 'Bar'), + ] + name = models.CharField(max_length=10) + choice = models.CharField(max_length=2, blank=True, choices=CHOICES) + choice_string_w_none = models.CharField( + max_length=2, blank=True, null=True, choices=STRING_CHOICES_WITH_NONE) + choice_integer = models.IntegerField(choices=INTEGER_CHOICES, blank=True, + null=True) @python_2_unicode_compatible diff --git a/tests/forms_tests/tests/__init__.py b/tests/forms_tests/tests/__init__.py index aea9418c05..39219f82be 100644 --- a/tests/forms_tests/tests/__init__.py +++ b/tests/forms_tests/tests/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from .test_error_messages import (FormsErrorMessagesTestCase, ModelChoiceFieldErrorMessagesTestCase) from .test_extra import FormsExtraTestCase, FormsExtraL10NTestCase diff --git a/tests/forms_tests/tests/test_error_messages.py b/tests/forms_tests/tests/test_error_messages.py index 9d45a46a3d..f0638298e9 100644 --- a/tests/forms_tests/tests/test_error_messages.py +++ b/tests/forms_tests/tests/test_error_messages.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index d439e2223c..3bc22243fb 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime @@ -620,6 +620,37 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): self.assertTrue(f.is_valid()) self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_changing_cleaned_data_nothing_returned(self): + class UserForm(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + def clean(self): + self.cleaned_data['username'] = self.cleaned_data['username'].lower() + # don't return anything + + f = UserForm({'username': 'SirRobin', 'password': 'blue'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['username'], 'sirrobin') + + def test_changing_cleaned_data_in_clean(self): + class UserForm(Form): + username = CharField(max_length=10) + password = CharField(widget=PasswordInput) + + def clean(self): + data = self.cleaned_data + + # Return a different dict. We have not changed self.cleaned_data. + return { + 'username': data['username'].lower(), + 'password': 'this_is_not_a_secret', + } + + f = UserForm({'username': 'SirRobin', 'password': 'blue'}) + self.assertTrue(f.is_valid()) + self.assertEqual(f.cleaned_data['username'], 'sirrobin') + def test_overriding_errorlist(self): @python_2_unicode_compatible class DivErrorList(ErrorList): diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 633fde5026..1c72d17abe 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import datetime from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.validators import RegexValidator from django.forms import * from django.http import QueryDict from django.template import Template, Context @@ -1792,6 +1793,75 @@ class FormsTestCase(TestCase): self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data, {'name' : 'fname lname'}) + def test_multivalue_optional_subfields(self): + class PhoneField(MultiValueField): + def __init__(self, *args, **kwargs): + fields = ( + CharField(label='Country Code', validators=[ + RegexValidator(r'^\+\d{1,2}$', message='Enter a valid country code.')]), + CharField(label='Phone Number'), + CharField(label='Extension', error_messages={'incomplete': 'Enter an extension.'}), + CharField(label='Label', required=False, help_text='E.g. home, work.'), + ) + super(PhoneField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + return '%s.%s ext. %s (label: %s)' % tuple(data_list) + return None + + # An empty value for any field will raise a `required` error on a + # required `MultiValueField`. + f = PhoneField() + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, []) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61']) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, ['+61', '287654321', '123']) + self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home'])) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # Empty values for fields will NOT raise a `required` error on an + # optional `MultiValueField` + f = PhoneField(required=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean([])) + self.assertEqual('+61. ext. (label: )', f.clean(['+61'])) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + self.assertEqual('+61.287654321 ext. 123 (label: Home)', f.clean(['+61', '287654321', '123', 'Home'])) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # For a required `MultiValueField` with `require_all_fields=False`, a + # `required` error will only be raised if all fields are empty. Fields + # can individually be required or optional. An empty value for any + # required field will raise an `incomplete` error. + f = PhoneField(require_all_fields=False) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) + self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, []) + self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61']) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + six.assertRaisesRegex(self, ValidationError, + "'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home']) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + + # For an optional `MultiValueField` with `require_all_fields=False`, we + # don't get any `required` error but we still get `incomplete` errors. + f = PhoneField(required=False, require_all_fields=False) + self.assertEqual(None, f.clean('')) + self.assertEqual(None, f.clean(None)) + self.assertEqual(None, f.clean([])) + self.assertRaisesMessage(ValidationError, "'Enter a complete value.'", f.clean, ['+61']) + self.assertEqual('+61.287654321 ext. 123 (label: )', f.clean(['+61', '287654321', '123'])) + six.assertRaisesRegex(self, ValidationError, + "'Enter a complete value\.', u?'Enter an extension\.'", f.clean, ['', '', '', 'Home']) + self.assertRaisesMessage(ValidationError, + "'Enter a valid country code.'", f.clean, ['61', '287654321', '123', 'Home']) + def test_custom_empty_values(self): """ Test that form fields can customize what is considered as an empty value @@ -1824,7 +1894,7 @@ class FormsTestCase(TestCase): # passing just one argument: overrides the field's label (('custom',), {}, '<label for="id_field">custom:</label>'), - # the overriden label is escaped + # the overridden label is escaped (('custom&',), {}, '<label for="id_field">custom&:</label>'), ((mark_safe('custom&'),), {}, '<label for="id_field">custom&:</label>'), @@ -1870,3 +1940,13 @@ class FormsTestCase(TestCase): boundfield = SomeForm()['field'] self.assertHTMLEqual(boundfield.label_tag(), '<label for="id_field"></label>') + + def test_label_tag_override(self): + """ + BoundField label_suffix (if provided) overrides Form label_suffix + """ + class SomeForm(Form): + field = CharField() + boundfield = SomeForm(label_suffix='!')['field'] + + self.assertHTMLEqual(boundfield.label_tag(label_suffix='$'), '<label for="id_field">Field$</label>') diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py index 4c566dc8e4..06ee2d6c53 100644 --- a/tests/forms_tests/tests/test_widgets.py +++ b/tests/forms_tests/tests/test_widgets.py @@ -849,6 +849,14 @@ beatle J R Ringo False""") w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'}) self.assertHTMLEqual(w.render('name', ['john', 'lennon']), '<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />') + # Test needs_multipart_form=True if any widget needs it + w = MyMultiWidget(widgets=(TextInput(), FileInput())) + self.assertTrue(w.needs_multipart_form) + + # Test needs_multipart_form=False if no widget needs it + w = MyMultiWidget(widgets=(TextInput(), TextInput())) + self.assertFalse(w.needs_multipart_form) + def test_splitdatetime(self): w = SplitDateTimeWidget() self.assertHTMLEqual(w.render('date', ''), '<input type="text" name="date_0" /><input type="text" name="date_1" />') diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index 99a67c320c..618ab8e07c 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime @@ -10,8 +10,8 @@ from django.forms.models import ModelFormMetaclass from django.test import TestCase from django.utils import six -from ..models import (ChoiceOptionModel, ChoiceFieldModel, FileModel, Group, - BoundaryModel, Defaults, OptionalMultiChoiceModel) +from ..models import (ChoiceModel, ChoiceOptionModel, ChoiceFieldModel, + FileModel, Group, BoundaryModel, Defaults, OptionalMultiChoiceModel) class ChoiceFieldForm(ModelForm): @@ -34,6 +34,24 @@ class ChoiceFieldExclusionForm(ModelForm): model = ChoiceFieldModel +class EmptyCharLabelChoiceForm(ModelForm): + class Meta: + model = ChoiceModel + fields = ['name', 'choice'] + + +class EmptyIntegerLabelChoiceForm(ModelForm): + class Meta: + model = ChoiceModel + fields = ['name', 'choice_integer'] + + +class EmptyCharLabelNoneChoiceForm(ModelForm): + class Meta: + model = ChoiceModel + fields = ['name', 'choice_string_w_none'] + + class FileForm(Form): file1 = FileField() @@ -259,3 +277,78 @@ class ManyToManyExclusionTestCase(TestCase): self.assertEqual(form.instance.choice_int.pk, data['choice_int']) self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3]) self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int']) + + +class EmptyLabelTestCase(TestCase): + def test_empty_field_char(self): + f = EmptyCharLabelChoiceForm() + self.assertHTMLEqual(f.as_p(), + """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p> +<p><label for="id_choice">Choice:</label> <select id="id_choice" name="choice"> +<option value="" selected="selected">No Preference</option> +<option value="f">Foo</option> +<option value="b">Bar</option> +</select></p>""") + + def test_empty_field_char_none(self): + f = EmptyCharLabelNoneChoiceForm() + self.assertHTMLEqual(f.as_p(), + """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p> +<p><label for="id_choice_string_w_none">Choice string w none:</label> <select id="id_choice_string_w_none" name="choice_string_w_none"> +<option value="" selected="selected">No Preference</option> +<option value="f">Foo</option> +<option value="b">Bar</option> +</select></p>""") + + def test_save_empty_label_forms(self): + # Test that saving a form with a blank choice results in the expected + # value being stored in the database. + tests = [ + (EmptyCharLabelNoneChoiceForm, 'choice_string_w_none', None), + (EmptyIntegerLabelChoiceForm, 'choice_integer', None), + (EmptyCharLabelChoiceForm, 'choice', ''), + ] + + for form, key, expected in tests: + f = form({'name': 'some-key', key: ''}) + self.assertTrue(f.is_valid()) + m = f.save() + self.assertEqual(expected, getattr(m, key)) + self.assertEqual('No Preference', + getattr(m, 'get_{0}_display'.format(key))()) + + def test_empty_field_integer(self): + f = EmptyIntegerLabelChoiceForm() + self.assertHTMLEqual(f.as_p(), + """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" /></p> +<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer"> +<option value="" selected="selected">No Preference</option> +<option value="1">Foo</option> +<option value="2">Bar</option> +</select></p>""") + + def test_get_display_value_on_none(self): + m = ChoiceModel.objects.create(name='test', choice='', choice_integer=None) + self.assertEqual(None, m.choice_integer) + self.assertEqual('No Preference', m.get_choice_integer_display()) + + def test_html_rendering_of_prepopulated_models(self): + none_model = ChoiceModel(name='none-test', choice_integer=None) + f = EmptyIntegerLabelChoiceForm(instance=none_model) + self.assertHTMLEqual(f.as_p(), + """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" value="none-test"/></p> +<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer"> +<option value="" selected="selected">No Preference</option> +<option value="1">Foo</option> +<option value="2">Bar</option> +</select></p>""") + + foo_model = ChoiceModel(name='foo-test', choice_integer=1) + f = EmptyIntegerLabelChoiceForm(instance=foo_model) + self.assertHTMLEqual(f.as_p(), + """<p><label for="id_name">Name:</label> <input id="id_name" maxlength="10" name="name" type="text" value="foo-test"/></p> +<p><label for="id_choice_integer">Choice integer:</label> <select id="id_choice_integer" name="choice_integer"> +<option value="">No Preference</option> +<option value="1" selected="selected">Foo</option> +<option value="2">Bar</option> +</select></p>""") diff --git a/tests/generic_inline_admin/admin.py b/tests/generic_inline_admin/admin.py index 73cac7f7c5..1917024fa5 100644 --- a/tests/generic_inline_admin/admin.py +++ b/tests/generic_inline_admin/admin.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from django.contrib.contenttypes import generic diff --git a/tests/generic_inline_admin/tests.py b/tests/generic_inline_admin/tests.py index ac2c191183..83bee2f4e0 100644 --- a/tests/generic_inline_admin/tests.py +++ b/tests/generic_inline_admin/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf import settings from django.contrib import admin @@ -325,3 +325,23 @@ class GenericInlineModelAdminTest(TestCase): self.assertEqual( list(list(ma.get_formsets(request))[0]().forms[0].fields), ['description', 'keywords', 'id', 'DELETE']) + + def test_get_fieldsets(self): + # Test that get_fieldsets is called when figuring out form fields. + # Refs #18681. + class MediaForm(ModelForm): + class Meta: + model = Media + fields = '__all__' + + class MediaInline(GenericTabularInline): + form = MediaForm + model = Media + can_delete = False + + def get_fieldsets(self, request, obj=None): + return [(None, {'fields': ['url', 'description']})] + + ma = MediaInline(Media, self.site) + form = ma.get_formset(None).form + self.assertEqual(form._meta.fields, ['url', 'description']) diff --git a/tests/generic_inline_admin/urls.py b/tests/generic_inline_admin/urls.py index 88d7b574d4..8d68d6c922 100644 --- a/tests/generic_inline_admin/urls.py +++ b/tests/generic_inline_admin/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, include from . import admin diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 734b2e5143..2b52ebac56 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django import forms from django.contrib.contenttypes.generic import generic_inlineformset_factory diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 013a8f282c..b1dd7a3040 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import time import unittest diff --git a/tests/generic_views/test_dates.py b/tests/generic_views/test_dates.py index bff909f7d0..03c6d02e5e 100644 --- a/tests/generic_views/test_dates.py +++ b/tests/generic_views/test_dates.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import time import datetime diff --git a/tests/generic_views/test_detail.py b/tests/generic_views/test_detail.py index 3a97d27995..8a487a7c3d 100644 --- a/tests/generic_views/test_detail.py +++ b/tests/generic_views/test_detail.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.test import TestCase diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index 9ed18833e4..9b1ba0f865 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import warnings from unittest import expectedFailure diff --git a/tests/generic_views/test_forms.py b/tests/generic_views/test_forms.py index 8c118e32a6..1ee26afc8f 100644 --- a/tests/generic_views/test_forms.py +++ b/tests/generic_views/test_forms.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django import forms diff --git a/tests/generic_views/test_list.py b/tests/generic_views/test_list.py index a77a6418a3..e572af8e32 100644 --- a/tests/generic_views/test_list.py +++ b/tests/generic_views/test_list.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.test import TestCase diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py index 695b50279a..4ef8f7a97f 100644 --- a/tests/generic_views/urls.py +++ b/tests/generic_views/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from django.views.decorators.cache import cache_page from django.views.generic import TemplateView diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index fd331f14b7..f839b53753 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py index 8d16af9587..eeb95c0818 100644 --- a/tests/get_earliest_or_latest/tests.py +++ b/tests/get_earliest_or_latest/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime diff --git a/tests/get_object_or_404/tests.py b/tests/get_object_or_404/tests.py index 38ebeb4f8c..1a0b41bc4d 100644 --- a/tests/get_object_or_404/tests.py +++ b/tests/get_object_or_404/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.http import Http404 from django.shortcuts import get_object_or_404, get_list_or_404 @@ -87,7 +87,7 @@ class GetObjectOr404Tests(TestCase): self.assertRaisesMessage(ValueError, "Object is of type 'str', but must be a Django Model, Manager, " "or QuerySet", - get_object_or_404, "Article", title__icontains="Run" + get_object_or_404, str("Article"), title__icontains="Run" ) class CustomClass(object): diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 0f766ab128..a612ea60a0 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import date import traceback @@ -65,7 +65,7 @@ class GetOrCreateTests(TestCase): ManualPrimaryKeyTest.objects.get_or_create(id=1, data="Different") except IntegrityError as e: formatted_traceback = traceback.format_exc() - self.assertIn('obj.save', formatted_traceback) + self.assertIn(str('obj.save'), formatted_traceback) def test_savepoint_rollback(self): # Regression test for #20463: the database connection should still be diff --git a/tests/get_or_create_regress/tests.py b/tests/get_or_create_regress/tests.py index 92c371b6f8..54dafc85fb 100644 --- a/tests/get_or_create_regress/tests.py +++ b/tests/get_or_create_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 3a2ec23864..1679b6e036 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -16,10 +16,13 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, SimpleCookie, BadHeaderError, parse_cookie) from django.test import TestCase -from django.utils.encoding import smart_str +from django.utils.encoding import smart_str, force_text +from django.utils.functional import lazy from django.utils._os import upath from django.utils import six +lazystr = lazy(force_text, six.text_type) + class QueryDictTests(unittest.TestCase): def test_missing_key(self): @@ -366,6 +369,10 @@ class HttpResponseTests(unittest.TestCase): self.assertEqual(list(i), [b'abc']) self.assertEqual(list(i), []) + def test_lazy_content(self): + r = HttpResponse(lazystr('helloworld')) + self.assertEqual(r.content, b'helloworld') + def test_file_interface(self): r = HttpResponse() r.write(b"hello") @@ -402,6 +409,11 @@ class HttpResponseSubclassesTests(TestCase): # Test that url attribute is right self.assertEqual(response.url, response['Location']) + def test_redirect_lazy(self): + """Make sure HttpResponseRedirect works with lazy strings.""" + r = HttpResponseRedirect(lazystr('/redirected/')) + self.assertEqual(r.url, '/redirected/') + def test_not_modified(self): response = HttpResponseNotModified() self.assertEqual(response.status_code, 304) diff --git a/tests/i18n/forms.py b/tests/i18n/forms.py index 6e4def9c5e..07b015d590 100644 --- a/tests/i18n/forms.py +++ b/tests/i18n/forms.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django import forms from django.forms.extras import SelectDateWidget diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 019ed88cdb..05d1065a75 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1,8 +1,9 @@ # -*- encoding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import decimal +from importlib import import_module import os import pickle from threading import local @@ -17,7 +18,6 @@ from django.utils import translation from django.utils.formats import (get_format, date_format, time_format, localize, localize_input, iter_format_modules, get_format_modules, number_format, reset_format_cache, sanitize_separators) -from django.utils.importlib import import_module from django.utils.numberformat import format as nformat from django.utils._os import upath from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText @@ -774,6 +774,39 @@ class FormattingTests(TransRealMixin, TestCase): self.assertEqual(template2.render(context), output2) self.assertEqual(template3.render(context), output3) + def test_localized_as_text_as_hidden_input(self): + """ + Tests if form input with 'as_hidden' or 'as_text' is correctly localized. Ticket #18777 + """ + self.maxDiff = 1200 + + with translation.override('de-at', deactivate=True): + template = Template('{% load l10n %}{{ form.date_added }}; {{ form.cents_paid }}') + template_as_text = Template('{% load l10n %}{{ form.date_added.as_text }}; {{ form.cents_paid.as_text }}') + template_as_hidden = Template('{% load l10n %}{{ form.date_added.as_hidden }}; {{ form.cents_paid.as_hidden }}') + form = CompanyForm({ + 'name': 'acme', + 'date_added': datetime.datetime(2009, 12, 31, 6, 0, 0), + 'cents_paid': decimal.Decimal('59.47'), + 'products_delivered': 12000, + }) + context = Context({'form': form }) + self.assertTrue(form.is_valid()) + + self.assertHTMLEqual( + template.render(context), + '<input id="id_date_added" name="date_added" type="text" value="31.12.2009 06:00:00" />; <input id="id_cents_paid" name="cents_paid" type="text" value="59,47" />' + ) + self.assertHTMLEqual( + template_as_text.render(context), + '<input id="id_date_added" name="date_added" type="text" value="31.12.2009 06:00:00" />; <input id="id_cents_paid" name="cents_paid" type="text" value="59,47" />' + ) + self.assertHTMLEqual( + template_as_hidden.render(context), + '<input id="id_date_added" name="date_added" type="hidden" value="31.12.2009 06:00:00" />; <input id="id_cents_paid" name="cents_paid" type="hidden" value="59,47" />' + ) + + class MiscTests(TransRealMixin, TestCase): def setUp(self): diff --git a/tests/inline_formsets/tests.py b/tests/inline_formsets/tests.py index ad8a666cb5..a16488dc79 100644 --- a/tests/inline_formsets/tests.py +++ b/tests/inline_formsets/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.forms.models import inlineformset_factory from django.test import TestCase diff --git a/tests/introspection/tests.py b/tests/introspection/tests.py index f1c87bbf14..6f38439945 100644 --- a/tests/introspection/tests.py +++ b/tests/introspection/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import unittest diff --git a/tests/known_related_objects/tests.py b/tests/known_related_objects/tests.py index d28d266557..6fd507cbdc 100644 --- a/tests/known_related_objects/tests.py +++ b/tests/known_related_objects/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index ee9c5afe1d..2beb43929b 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime from operator import attrgetter @@ -470,7 +470,8 @@ class LookupTests(TestCase): 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") + "into field. Choices are: author, author_id, headline, " + "id, pub_date, tag") try: Article.objects.filter(headline__starts='Article') self.fail('FieldError not raised') diff --git a/tests/m2m_and_m2o/tests.py b/tests/m2m_and_m2o/tests.py index 77f2eb3b09..0380ad4b08 100644 --- a/tests/m2m_and_m2o/tests.py +++ b/tests/m2m_and_m2o/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.db.models import Q from django.test import TestCase diff --git a/tests/m2m_intermediary/tests.py b/tests/m2m_intermediary/tests.py index f261f23546..d9c77ecb7c 100644 --- a/tests/m2m_intermediary/tests.py +++ b/tests/m2m_intermediary/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime diff --git a/tests/m2m_multiple/tests.py b/tests/m2m_multiple/tests.py index 7bf88f99bb..2122517ce4 100644 --- a/tests/m2m_multiple/tests.py +++ b/tests/m2m_multiple/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime diff --git a/tests/m2m_recursive/tests.py b/tests/m2m_recursive/tests.py index a3f2c670d6..3beafc9692 100644 --- a/tests/m2m_recursive/tests.py +++ b/tests/m2m_recursive/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/m2m_regress/tests.py b/tests/m2m_regress/tests.py index 884fc097a1..610f01694a 100644 --- a/tests/m2m_regress/tests.py +++ b/tests/m2m_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import FieldError from django.test import TestCase diff --git a/tests/m2m_signals/tests.py b/tests/m2m_signals/tests.py index d3d2a74c70..569a2dc12d 100644 --- a/tests/m2m_signals/tests.py +++ b/tests/m2m_signals/tests.py @@ -2,8 +2,6 @@ Testing signals emitted on changing m2m relations. """ -from .models import Person - from django.db import models from django.test import TestCase diff --git a/tests/m2m_through/tests.py b/tests/m2m_through/tests.py index 259dc68a0b..69a7ec99bf 100644 --- a/tests/m2m_through/tests.py +++ b/tests/m2m_through/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime from operator import attrgetter diff --git a/tests/m2m_through_regress/tests.py b/tests/m2m_through_regress/tests.py index de4d52a2db..c1b229b30b 100644 --- a/tests/m2m_through_regress/tests.py +++ b/tests/m2m_through_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core import management from django.contrib.auth.models import User diff --git a/tests/m2o_recursive/tests.py b/tests/m2o_recursive/tests.py index fa04c74cca..f5e8938706 100644 --- a/tests/m2o_recursive/tests.py +++ b/tests/m2o_recursive/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 822c572b0f..0f85cc0c76 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -397,6 +397,34 @@ class BaseEmailBackendTests(object): self.assertEqual(message.get_payload(), "Content") self.assertEqual(message["from"], "=?utf-8?q?Firstname_S=C3=BCrname?= <from@example.com>") + def test_plaintext_send_mail(self): + """ + Test send_mail without the html_message + regression test for adding html_message parameter to send_mail() + """ + send_mail('Subject', 'Content', 'sender@example.com', ['nobody@example.com']) + message = self.get_the_message() + + self.assertEqual(message.get('subject'), 'Subject') + self.assertEqual(message.get_all('to'), ['nobody@example.com']) + self.assertFalse(message.is_multipart()) + self.assertEqual(message.get_payload(), 'Content') + self.assertEqual(message.get_content_type(), 'text/plain') + + def test_html_send_mail(self): + """Test html_message argument to send_mail""" + send_mail('Subject', 'Content', 'sender@example.com', ['nobody@example.com'], html_message='HTML Content') + message = self.get_the_message() + + self.assertEqual(message.get('subject'), 'Subject') + self.assertEqual(message.get_all('to'), ['nobody@example.com']) + self.assertTrue(message.is_multipart()) + self.assertEqual(len(message.get_payload()), 2) + self.assertEqual(message.get_payload(0).get_payload(), 'Content') + self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain') + self.assertEqual(message.get_payload(1).get_payload(), 'HTML Content') + self.assertEqual(message.get_payload(1).get_content_type(), 'text/html') + @override_settings(MANAGERS=[('nobody', 'nobody@example.com')]) def test_html_mail_managers(self): """Test html_message argument to mail_managers""" diff --git a/tests/managers_regress/tests.py b/tests/managers_regress/tests.py index 45059be4e5..e5dce53122 100644 --- a/tests/managers_regress/tests.py +++ b/tests/managers_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import copy from django.conf import settings diff --git a/tests/many_to_many/tests.py b/tests/many_to_many/tests.py index 7d30379b94..e6fed191fc 100644 --- a/tests/many_to_many/tests.py +++ b/tests/many_to_many/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase from django.utils import six diff --git a/tests/many_to_one/tests.py b/tests/many_to_one/tests.py index a4f87a3283..ae629288b1 100644 --- a/tests/many_to_one/tests.py +++ b/tests/many_to_one/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from copy import deepcopy import datetime diff --git a/tests/many_to_one_null/tests.py b/tests/many_to_one_null/tests.py index 4de44b5e64..cd1800d77c 100644 --- a/tests/many_to_one_null/tests.py +++ b/tests/many_to_one_null/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/many_to_one_regress/tests.py b/tests/many_to_one_regress/tests.py index 035ba53bff..19cfaad097 100644 --- a/tests/many_to_one_regress/tests.py +++ b/tests/many_to_one_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import models from django.test import TestCase diff --git a/tests/max_lengths/tests.py b/tests/max_lengths/tests.py index 0f525864d5..43f9e217c8 100644 --- a/tests/max_lengths/tests.py +++ b/tests/max_lengths/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import unittest diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 5ceab2e594..0897ec06be 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import gzip from io import BytesIO diff --git a/tests/middleware_exceptions/urls.py b/tests/middleware_exceptions/urls.py index 042607fdc8..fcef638553 100644 --- a/tests/middleware_exceptions/urls.py +++ b/tests/middleware_exceptions/urls.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import absolute_import - from django.conf.urls import patterns from . import views diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py index f6019bd77f..ce7d33eb32 100644 --- a/tests/model_fields/test_imagefield.py +++ b/tests/model_fields/test_imagefield.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import os import shutil diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index 1ce5eba3e9..378e6280cc 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime from decimal import Decimal @@ -8,11 +8,21 @@ from django import test from django import forms from django.core.exceptions import ValidationError from django.db import connection, models, IntegrityError -from django.db.models.fields.files import FieldFile +from django.db.models.fields import ( + AutoField, BigIntegerField, BinaryField, BooleanField, CharField, + CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField, + EmailField, FilePathField, FloatField, IntegerField, IPAddressField, + GenericIPAddressField, NullBooleanField, PositiveIntegerField, + PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField, + TimeField, URLField) +from django.db.models.fields.files import FileField, ImageField from django.utils import six +from django.utils.functional import lazy +from django.utils.unittest import skipIf -from .models import (Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, - NullBooleanModel, BooleanModel, DataModel, Document, RenamedField, +from .models import ( + Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel, + BooleanModel, DataModel, Document, RenamedField, VerboseNameField, FksToBooleans) @@ -64,7 +74,7 @@ class BasicFieldTests(test.TestCase): m = VerboseNameField for i in range(1, 23): self.assertEqual(m._meta.get_field('field%d' % i).verbose_name, - 'verbose field%d' % i) + 'verbose field%d' % i) self.assertEqual(m._meta.get_field('id').verbose_name, 'verbose pk') @@ -290,9 +300,9 @@ class SlugFieldTests(test.TestCase): """ Make sure SlugField honors max_length (#9706) """ - bs = BigS.objects.create(s = 'slug'*50) + bs = BigS.objects.create(s='slug' * 50) bs = BigS.objects.get(pk=bs.pk) - self.assertEqual(bs.s, 'slug'*50) + self.assertEqual(bs.s, 'slug' * 50) class ValidationTest(test.TestCase): @@ -313,15 +323,21 @@ class ValidationTest(test.TestCase): self.assertRaises(ValidationError, f.clean, "a", None) def test_charfield_with_choices_cleans_valid_choice(self): - f = models.CharField(max_length=1, choices=[('a','A'), ('b','B')]) + f = models.CharField(max_length=1, + choices=[('a', 'A'), ('b', 'B')]) self.assertEqual('a', f.clean('a', None)) def test_charfield_with_choices_raises_error_on_invalid_choice(self): - f = models.CharField(choices=[('a','A'), ('b','B')]) + f = models.CharField(choices=[('a', 'A'), ('b', 'B')]) self.assertRaises(ValidationError, f.clean, "not a", None) + def test_charfield_get_choices_with_blank_defined(self): + f = models.CharField(choices=[('', '<><>'), ('a', 'A')]) + self.assertEqual(f.get_choices(True), [('', '<><>'), ('a', 'A')]) + def test_choices_validation_supports_named_groups(self): - f = models.IntegerField(choices=(('group',((10,'A'),(20,'B'))),(30,'C'))) + f = models.IntegerField( + choices=(('group', ((10, 'A'), (20, 'B'))), (30, 'C'))) self.assertEqual(10, f.clean(10, None)) def test_nullable_integerfield_raises_error_with_blank_false(self): @@ -370,7 +386,7 @@ class BigIntegerFieldTests(test.TestCase): self.assertEqual(qs[0].value, minval) def test_types(self): - b = BigInt(value = 0) + b = BigInt(value=0) self.assertIsInstance(b.value, six.integer_types) b.save() self.assertIsInstance(b.value, six.integer_types) @@ -378,8 +394,8 @@ class BigIntegerFieldTests(test.TestCase): self.assertIsInstance(b.value, six.integer_types) def test_coercing(self): - BigInt.objects.create(value ='10') - b = BigInt.objects.get(value = '10') + BigInt.objects.create(value='10') + b = BigInt.objects.get(value='10') self.assertEqual(b.value, 10) class TypeCoercionTests(test.TestCase): @@ -466,7 +482,7 @@ class BinaryFieldTests(test.TestCase): test_set_and_retrieve = unittest.expectedFailure(test_set_and_retrieve) def test_max_length(self): - dm = DataModel(short_data=self.binary_data*4) + dm = DataModel(short_data=self.binary_data * 4) self.assertRaises(ValidationError, dm.full_clean) class GenericIPAddressFieldTests(test.TestCase): @@ -481,3 +497,156 @@ class GenericIPAddressFieldTests(test.TestCase): model_field = models.GenericIPAddressField(protocol='IPv6') form_field = model_field.formfield() self.assertRaises(ValidationError, form_field.clean, '127.0.0.1') + + +class PromiseTest(test.TestCase): + def test_AutoField(self): + lazy_func = lazy(lambda: 1, int) + self.assertIsInstance( + AutoField(primary_key=True).get_prep_value(lazy_func()), + int) + + @skipIf(six.PY3, "Python 3 has no `long` type.") + def test_BigIntegerField(self): + lazy_func = lazy(lambda: long(9999999999999999999), long) + self.assertIsInstance( + BigIntegerField().get_prep_value(lazy_func()), + long) + + def test_BinaryField(self): + lazy_func = lazy(lambda: b'', bytes) + self.assertIsInstance( + BinaryField().get_prep_value(lazy_func()), + bytes) + + def test_BooleanField(self): + lazy_func = lazy(lambda: True, bool) + self.assertIsInstance( + BooleanField().get_prep_value(lazy_func()), + bool) + + def test_CharField(self): + lazy_func = lazy(lambda: '', six.text_type) + self.assertIsInstance( + CharField().get_prep_value(lazy_func()), + six.text_type) + + def test_CommaSeparatedIntegerField(self): + lazy_func = lazy(lambda: '1,2', six.text_type) + self.assertIsInstance( + CommaSeparatedIntegerField().get_prep_value(lazy_func()), + six.text_type) + + def test_DateField(self): + lazy_func = lazy(lambda: datetime.date.today(), datetime.date) + self.assertIsInstance( + DateField().get_prep_value(lazy_func()), + datetime.date) + + def test_DateTimeField(self): + lazy_func = lazy(lambda: datetime.datetime.now(), datetime.datetime) + self.assertIsInstance( + DateTimeField().get_prep_value(lazy_func()), + datetime.datetime) + + def test_DecimalField(self): + lazy_func = lazy(lambda: Decimal('1.2'), Decimal) + self.assertIsInstance( + DecimalField().get_prep_value(lazy_func()), + Decimal) + + def test_EmailField(self): + lazy_func = lazy(lambda: 'mailbox@domain.com', six.text_type) + self.assertIsInstance( + EmailField().get_prep_value(lazy_func()), + six.text_type) + + def test_FileField(self): + lazy_func = lazy(lambda: 'filename.ext', six.text_type) + self.assertIsInstance( + FileField().get_prep_value(lazy_func()), + six.text_type) + + def test_FilePathField(self): + lazy_func = lazy(lambda: 'tests.py', six.text_type) + self.assertIsInstance( + FilePathField().get_prep_value(lazy_func()), + six.text_type) + + def test_FloatField(self): + lazy_func = lazy(lambda: 1.2, float) + self.assertIsInstance( + FloatField().get_prep_value(lazy_func()), + float) + + def test_ImageField(self): + lazy_func = lazy(lambda: 'filename.ext', six.text_type) + self.assertIsInstance( + ImageField().get_prep_value(lazy_func()), + six.text_type) + + def test_IntegerField(self): + lazy_func = lazy(lambda: 1, int) + self.assertIsInstance( + IntegerField().get_prep_value(lazy_func()), + int) + + def test_IPAddressField(self): + lazy_func = lazy(lambda: '127.0.0.1', six.text_type) + self.assertIsInstance( + IPAddressField().get_prep_value(lazy_func()), + six.text_type) + + def test_GenericIPAddressField(self): + lazy_func = lazy(lambda: '127.0.0.1', six.text_type) + self.assertIsInstance( + GenericIPAddressField().get_prep_value(lazy_func()), + six.text_type) + + def test_NullBooleanField(self): + lazy_func = lazy(lambda: True, bool) + self.assertIsInstance( + NullBooleanField().get_prep_value(lazy_func()), + bool) + + def test_PositiveIntegerField(self): + lazy_func = lazy(lambda: 1, int) + self.assertIsInstance( + PositiveIntegerField().get_prep_value(lazy_func()), + int) + + def test_PositiveSmallIntegerField(self): + lazy_func = lazy(lambda: 1, int) + self.assertIsInstance( + PositiveSmallIntegerField().get_prep_value(lazy_func()), + int) + + def test_SlugField(self): + lazy_func = lazy(lambda: 'slug', six.text_type) + self.assertIsInstance( + SlugField().get_prep_value(lazy_func()), + six.text_type) + + def test_SmallIntegerField(self): + lazy_func = lazy(lambda: 1, int) + self.assertIsInstance( + SmallIntegerField().get_prep_value(lazy_func()), + int) + + def test_TextField(self): + lazy_func = lazy(lambda: 'Abc', six.text_type) + self.assertIsInstance( + TextField().get_prep_value(lazy_func()), + six.text_type) + + def test_TimeField(self): + lazy_func = lazy(lambda: datetime.datetime.now().time(), datetime.time) + self.assertIsInstance( + TimeField().get_prep_value(lazy_func()), + datetime.time) + + def test_URLField(self): + lazy_func = lazy(lambda: 'http://domain.com', six.text_type) + self.assertIsInstance( + URLField().get_prep_value(lazy_func()), + six.text_type) diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index a4cf9471de..17aec29055 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -12,7 +12,7 @@ import os import tempfile from django.core import validators -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.files.storage import FileSystemStorage from django.db import models from django.utils import six @@ -296,3 +296,7 @@ class CustomErrorMessage(models.Model): name2 = models.CharField(max_length=50, validators=[validators.validate_slug], error_messages={'invalid': 'Model custom error message.'}) + + def clean(self): + if self.name1 == 'FORBIDDEN_VALUE': + raise ValidationError({'name1': [ValidationError('Model.clean() error messages.')]}) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 09c62c5205..1404725484 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import os @@ -1782,6 +1782,14 @@ class OldFormForXTests(TestCase): '<ul class="errorlist"><li>Model custom error message.</li></ul>' ) + def test_model_clean_error_messages(self) : + data = {'name1': 'FORBIDDEN_VALUE', 'name2': 'ABC'} + errors = CustomErrorMessageForm(data).errors + self.assertHTMLEqual( + str(errors['name1']), + '<ul class="errorlist"><li>Model.clean() error messages.</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 35e706ac4c..ffe1123b99 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import date import unittest diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 4411bbf59a..053448181a 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import re @@ -32,6 +32,9 @@ class DeletionTests(TestCase): 'form-0-DELETE': 'on', } formset = PoetFormSet(data, queryset=Poet.objects.all()) + formset.save(commit=False) + self.assertEqual(Poet.objects.count(), 1) + formset.save() self.assertTrue(formset.is_valid()) self.assertEqual(Poet.objects.count(), 0) @@ -388,7 +391,7 @@ class ModelFormsetTest(TestCase): def test_custom_queryset_init(self): """ - Test that a queryset can be overriden in the __init__ method. + Test that a queryset can be overridden in the __init__ method. https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#changing-the-queryset """ author1 = Author.objects.create(name='Charles Baudelaire') diff --git a/tests/model_formsets_regress/tests.py b/tests/model_formsets_regress/tests.py index 179f79fbcb..782c7d6fbc 100644 --- a/tests/model_formsets_regress/tests.py +++ b/tests/model_formsets_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django import forms from django.forms.formsets import BaseFormSet, DELETION_FIELD_NAME diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index dc40d2d2e0..eb50d30cf8 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/model_inheritance_regress/tests.py b/tests/model_inheritance_regress/tests.py index 7ca16647fa..10a1230685 100644 --- a/tests/model_inheritance_regress/tests.py +++ b/tests/model_inheritance_regress/tests.py @@ -1,7 +1,7 @@ """ Regression tests for Model inheritance behavior. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime from operator import attrgetter diff --git a/tests/model_inheritance_same_model_name/models.py b/tests/model_inheritance_same_model_name/models.py index b4a6b930df..8b02b08668 100644 --- a/tests/model_inheritance_same_model_name/models.py +++ b/tests/model_inheritance_same_model_name/models.py @@ -6,8 +6,6 @@ in the need for an %(app_label)s format string. This app specifically tests this feature by redefining the Copy model from model_inheritance/models.py """ -from __future__ import absolute_import - from django.db import models from model_inheritance.models import NamedURL diff --git a/tests/model_inheritance_same_model_name/tests.py b/tests/model_inheritance_same_model_name/tests.py index 8f22578013..fe1fb0cdd2 100644 --- a/tests/model_inheritance_same_model_name/tests.py +++ b/tests/model_inheritance_same_model_name/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/model_inheritance_select_related/tests.py b/tests/model_inheritance_select_related/tests.py index 078b466d0e..68f9897e25 100644 --- a/tests/model_inheritance_select_related/tests.py +++ b/tests/model_inheritance_select_related/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/model_package/models/__init__.py b/tests/model_package/models/__init__.py index 3c261aa444..ec29d667f9 100644 --- a/tests/model_package/models/__init__.py +++ b/tests/model_package/models/__init__.py @@ -1,5 +1,3 @@ # Import all the models from subpackages -from __future__ import absolute_import - from .article import Article from .publication import Publication diff --git a/tests/model_package/tests.py b/tests/model_package/tests.py index 5d856a9608..a352b57478 100644 --- a/tests/model_package/tests.py +++ b/tests/model_package/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.contrib.sites.models import Site from django.db import models diff --git a/tests/model_permalink/tests.py b/tests/model_permalink/tests.py index 257648ca5f..ef682ed0e8 100644 --- a/tests/model_permalink/tests.py +++ b/tests/model_permalink/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.test import TestCase from .models import Guitarist diff --git a/tests/model_regress/tests.py b/tests/model_regress/tests.py index 2924b220e6..f84a40b05b 100644 --- a/tests/model_regress/tests.py +++ b/tests/model_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime from operator import attrgetter diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 0d0fed394a..424588cf49 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import date import unittest @@ -51,6 +51,12 @@ class ModelAdminTests(TestCase): self.assertEqual(list(ma.get_form(request).base_fields), ['name', 'bio', 'sign_date']) + self.assertEqual(list(ma.get_fields(request)), + ['name', 'bio', 'sign_date']) + + self.assertEqual(list(ma.get_fields(request, self.band)), + ['name', 'bio', 'sign_date']) + def test_default_fieldsets(self): # fieldsets_add and fieldsets_change should return a special data structure that # is used in the templates. They should generate the "right thing" whether we @@ -97,6 +103,10 @@ class ModelAdminTests(TestCase): ma = BandAdmin(Band, self.site) + self.assertEqual(list(ma.get_fields(request)), ['name']) + + self.assertEqual(list(ma.get_fields(request, self.band)), ['name']) + self.assertEqual(ma.get_fieldsets(request), [(None, {'fields': ['name']})]) diff --git a/tests/multiple_database/models.py b/tests/multiple_database/models.py index e46438649b..00534c870c 100644 --- a/tests/multiple_database/models.py +++ b/tests/multiple_database/models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py index e65fa4e19d..629cb1237c 100644 --- a/tests/multiple_database/tests.py +++ b/tests/multiple_database/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import pickle diff --git a/tests/mutually_referential/tests.py b/tests/mutually_referential/tests.py index b3deb0e75c..6006465843 100644 --- a/tests/mutually_referential/tests.py +++ b/tests/mutually_referential/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.test import TestCase from .models import Parent diff --git a/tests/nested_foreign_keys/tests.py b/tests/nested_foreign_keys/tests.py index 5cb23cfb9c..e8e20762fe 100644 --- a/tests/nested_foreign_keys/tests.py +++ b/tests/nested_foreign_keys/tests.py @@ -1,4 +1,5 @@ -from __future__ import absolute_import +from __future__ import unicode_literals + from django.test import TestCase from .models import Person, Movie, Event, Screening, ScreeningNullFK, Package, PackageNullFK diff --git a/tests/null_fk/tests.py b/tests/null_fk/tests.py index 96a06b6769..29e1fcb4bb 100644 --- a/tests/null_fk/tests.py +++ b/tests/null_fk/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.db.models import Q from django.test import TestCase diff --git a/tests/null_fk_ordering/tests.py b/tests/null_fk_ordering/tests.py index aea969de4f..70873244ac 100644 --- a/tests/null_fk_ordering/tests.py +++ b/tests/null_fk_ordering/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/null_queries/tests.py b/tests/null_queries/tests.py index 93e72d55d8..d08c9979d7 100644 --- a/tests/null_queries/tests.py +++ b/tests/null_queries/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase from django.core.exceptions import FieldError diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index a36764b788..6e16d81cea 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import transaction, IntegrityError from django.test import TestCase diff --git a/tests/one_to_one_regress/tests.py b/tests/one_to_one_regress/tests.py index 615536ba38..7d82194abb 100644 --- a/tests/one_to_one_regress/tests.py +++ b/tests/one_to_one_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/or_lookups/tests.py b/tests/or_lookups/tests.py index e1c6fcb32a..264d999575 100644 --- a/tests/or_lookups/tests.py +++ b/tests/or_lookups/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime from operator import attrgetter diff --git a/tests/order_with_respect_to/tests.py b/tests/order_with_respect_to/tests.py index 559cb1d996..155c238617 100644 --- a/tests/order_with_respect_to/tests.py +++ b/tests/order_with_respect_to/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from operator import attrgetter diff --git a/tests/ordering/tests.py b/tests/ordering/tests.py index b1b5253682..63161a3273 100644 --- a/tests/ordering/tests.py +++ b/tests/ordering/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import datetime from operator import attrgetter diff --git a/tests/pagination/tests.py b/tests/pagination/tests.py index 76799bec46..46ea428d17 100644 --- a/tests/pagination/tests.py +++ b/tests/pagination/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from datetime import datetime import unittest diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index 2e3fee6be6..9d5a5290da 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType from django.db import connection diff --git a/tests/properties/tests.py b/tests/properties/tests.py index 8a40d06e98..c471c414a1 100644 --- a/tests/properties/tests.py +++ b/tests/properties/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/proxy_model_inheritance/app1/models.py b/tests/proxy_model_inheritance/app1/models.py index affcf140ac..a7a99fe46b 100644 --- a/tests/proxy_model_inheritance/app1/models.py +++ b/tests/proxy_model_inheritance/app1/models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - # TODO: why can't I make this ..app2 from app2.models import NiceModel diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py index 85bf74a4e9..dda09a080f 100644 --- a/tests/proxy_model_inheritance/tests.py +++ b/tests/proxy_model_inheritance/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import os import sys diff --git a/tests/proxy_models/tests.py b/tests/proxy_models/tests.py index 5cc5ef5478..240198cc39 100644 --- a/tests/proxy_models/tests.py +++ b/tests/proxy_models/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import copy from django.conf import settings diff --git a/tests/queries/tests.py b/tests/queries/tests.py index 87d54b637e..6e03b0d7f6 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -1,5 +1,6 @@ -from __future__ import absolute_import,unicode_literals +from __future__ import unicode_literals +from collections import OrderedDict import datetime from operator import attrgetter import pickle @@ -14,7 +15,6 @@ from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature from django.test.utils import str_prefix -from django.utils.datastructures import SortedDict from .models import ( Annotation, Article, Author, Celebrity, Child, Cover, Detail, DumbCategory, @@ -499,7 +499,7 @@ class Queries1Tests(BaseQuerysetTest): ) def test_ticket2902(self): - # Parameters can be given to extra_select, *if* you use a SortedDict. + # Parameters can be given to extra_select, *if* you use an OrderedDict. # (First we need to know which order the keys fall in "naturally" on # your system, so we can put things in the wrong way around from @@ -513,7 +513,7 @@ class Queries1Tests(BaseQuerysetTest): # This slightly odd comparison works around the fact that PostgreSQL will # return 'one' and 'two' as strings, not Unicode objects. It's a side-effect of # using constants here and not a real concern. - d = Item.objects.extra(select=SortedDict(s), select_params=params).values('a', 'b')[0] + d = Item.objects.extra(select=OrderedDict(s), select_params=params).values('a', 'b')[0] self.assertEqual(d, {'a': 'one', 'b': 'two'}) # Order by the number of tags attached to an item. @@ -789,7 +789,7 @@ class Queries1Tests(BaseQuerysetTest): ) def test_ticket7181(self): - # Ordering by related tables should accomodate nullable fields (this + # Ordering by related tables should accommodate nullable fields (this # test is a little tricky, since NULL ordering is database dependent. # Instead, we just count the number of results). self.assertEqual(len(Tag.objects.order_by('parent__name')), 5) @@ -1987,7 +1987,7 @@ class ValuesQuerysetTests(BaseQuerysetTest): def test_extra_values(self): # testing for ticket 14930 issues - qs = Number.objects.extra(select=SortedDict([('value_plus_x', 'num+%s'), + qs = Number.objects.extra(select=OrderedDict([('value_plus_x', 'num+%s'), ('value_minus_x', 'num-%s')]), select_params=(1, 2)) qs = qs.order_by('value_minus_x') @@ -2910,7 +2910,7 @@ class DoubleInSubqueryTests(TestCase): self.assertQuerysetEqual( qs, [lfb1], lambda x: x) -class Ticket18785Tests(unittest.TestCase): +class Ticket18785Tests(TestCase): def test_ticket_18785(self): # Test join trimming from ticket18785 qs = Item.objects.exclude( @@ -2920,3 +2920,38 @@ class Ticket18785Tests(unittest.TestCase): ).order_by() self.assertEqual(1, str(qs.query).count('INNER JOIN')) self.assertEqual(0, str(qs.query).count('OUTER JOIN')) + + +class Ticket20788Tests(TestCase): + def test_ticket_20788(self): + Paragraph.objects.create() + paragraph = Paragraph.objects.create() + page = paragraph.page.create() + chapter = Chapter.objects.create(paragraph=paragraph) + Book.objects.create(chapter=chapter) + + paragraph2 = Paragraph.objects.create() + Page.objects.create() + chapter2 = Chapter.objects.create(paragraph=paragraph2) + book2 = Book.objects.create(chapter=chapter2) + + sentences_not_in_pub = Book.objects.exclude( + chapter__paragraph__page=page) + self.assertQuerysetEqual( + sentences_not_in_pub, [book2], lambda x: x) + +class RelatedLookupTypeTests(TestCase): + def test_wrong_type_lookup(self): + oa = ObjectA.objects.create(name="oa") + wrong_type = Order.objects.create(id=oa.pk) + ob = ObjectB.objects.create(name="ob", objecta=oa, num=1) + # Currently Django doesn't care if the object is of correct + # type, it will just use the objecta's related fields attribute + # (id) for model lookup. Making things more restrictive could + # be a good idea... + self.assertQuerysetEqual( + ObjectB.objects.filter(objecta=wrong_type), + [ob], lambda x: x) + self.assertQuerysetEqual( + ObjectB.objects.filter(objecta__in=[wrong_type]), + [ob], lambda x: x) diff --git a/tests/queryset_pickle/models.py b/tests/queryset_pickle/models.py index 3a8973505c..dacd5018da 100644 --- a/tests/queryset_pickle/models.py +++ b/tests/queryset_pickle/models.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import datetime from django.db import models diff --git a/tests/queryset_pickle/tests.py b/tests/queryset_pickle/tests.py index b4b540c80d..384073ad56 100644 --- a/tests/queryset_pickle/tests.py +++ b/tests/queryset_pickle/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import pickle import datetime @@ -83,10 +83,14 @@ class PickleabilityTestCase(TestCase): def test_model_pickle_dynamic(self): class Meta: proxy = True - dynclass = type("DynamicEventSubclass", (Event, ), + dynclass = type(str("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) + + def test_specialized_queryset(self): + self.assert_pickles(Happening.objects.values('name')) + self.assert_pickles(Happening.objects.values('name').dates('when', 'year')) diff --git a/tests/raw_query/tests.py b/tests/raw_query/tests.py index 7242b8309b..06c9b11230 100644 --- a/tests/raw_query/tests.py +++ b/tests/raw_query/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from datetime import date diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 8c56e48f58..5bd3e5141f 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -620,12 +620,20 @@ class HostValidationTests(SimpleTestCase): } self.assertEqual(request.get_host(), 'example.com') + # Invalid hostnames would normally raise a SuspiciousOperation, + # but we have DEBUG=True, so this check is disabled. + request = HttpRequest() + request.META = { + 'HTTP_HOST': "invalid_hostname.com", + } + self.assertEqual(request.get_host(), "invalid_hostname.com") @override_settings(ALLOWED_HOSTS=[]) def test_get_host_suggestion_of_allowed_host(self): """get_host() makes helpful suggestions if a valid-looking host is not in ALLOWED_HOSTS.""" msg_invalid_host = "Invalid HTTP_HOST header: %r." msg_suggestion = msg_invalid_host + "You may need to add %r to ALLOWED_HOSTS." + msg_suggestion2 = msg_invalid_host + "The domain name provided is not valid according to RFC 1034/1035" for host in [ # Valid-looking hosts 'example.com', @@ -664,6 +672,14 @@ class HostValidationTests(SimpleTestCase): request.get_host ) + request = HttpRequest() + request.META = {'HTTP_HOST': "invalid_hostname.com"} + self.assertRaisesMessage( + SuspiciousOperation, + msg_suggestion2 % "invalid_hostname.com", + request.get_host + ) + @skipIf(connection.vendor == 'sqlite' and connection.settings_dict['TEST_NAME'] in (None, '', ':memory:'), diff --git a/tests/reserved_names/tests.py b/tests/reserved_names/tests.py index ddffe08d34..cdf81b8477 100644 --- a/tests/reserved_names/tests.py +++ b/tests/reserved_names/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import datetime diff --git a/tests/reverse_lookup/tests.py b/tests/reverse_lookup/tests.py index 549ee66392..ca16db0d31 100644 --- a/tests/reverse_lookup/tests.py +++ b/tests/reverse_lookup/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.core.exceptions import FieldError from django.test import TestCase diff --git a/tests/reverse_single_related/tests.py b/tests/reverse_single_related/tests.py index 0c755c4db6..472a3026b6 100644 --- a/tests/reverse_single_related/tests.py +++ b/tests/reverse_single_related/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.test import TestCase from .models import Source, Item diff --git a/tests/runtests.py b/tests/runtests.py index 8d5b375fb3..53318a7461 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -7,6 +7,20 @@ import sys import tempfile import warnings +def upath(path): + """ + Separate version of django.utils._os.upath. The django.utils version isn't + usable here, as upath is needed for RUNTESTS_DIR which is needed before + django can be imported. + """ + if sys.version_info[0] != 3 and not isinstance(path, bytes): + fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + return path.decode(fs_encoding) + return path + +RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) +sys.path.insert(0, os.path.dirname(RUNTESTS_DIR)) # 'tests/../' + from django import contrib from django.utils._os import upath from django.utils import six @@ -15,7 +29,6 @@ CONTRIB_MODULE_PATH = 'django.contrib' TEST_TEMPLATE_DIR = 'templates' -RUNTESTS_DIR = os.path.abspath(os.path.dirname(upath(__file__))) CONTRIB_DIR = os.path.dirname(upath(contrib.__file__)) TEMP_DIR = tempfile.mkdtemp(prefix='django_') @@ -331,10 +344,9 @@ if __name__ == "__main__": options, args = parser.parse_args() if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings - elif "DJANGO_SETTINGS_MODULE" not in os.environ: - parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. " - "Set it or use --settings.") else: + if "DJANGO_SETTINGS_MODULE" not in os.environ: + os.environ['DJANGO_SETTINGS_MODULE'] = 'test_sqlite' options.settings = os.environ['DJANGO_SETTINGS_MODULE'] if options.liveserver is not None: diff --git a/tests/save_delete_hooks/tests.py b/tests/save_delete_hooks/tests.py index 42e0d4a80e..0fd1ed4e03 100644 --- a/tests/save_delete_hooks/tests.py +++ b/tests/save_delete_hooks/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.test import TestCase from django.utils import six diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py index 3204d74224..f9e7e96ac1 100644 --- a/tests/select_for_update/tests.py +++ b/tests/select_for_update/tests.py @@ -1,15 +1,17 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import sys import time import unittest from django.conf import settings -from django.db import transaction, connection +from django.db import transaction, connection, router from django.db.utils import ConnectionHandler, DEFAULT_DB_ALIAS, DatabaseError from django.test import (TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature) +from multiple_database.tests import TestRouter + from .models import Person # Some tests require threading, which might not be available. So create a @@ -254,3 +256,13 @@ class SelectForUpdateTests(TransactionTestCase): """ people = list(Person.objects.select_for_update()) self.assertTrue(transaction.is_dirty()) + + @skipUnlessDBFeature('has_select_for_update') + def test_select_for_update_on_multidb(self): + old_routers = router.routers + try: + router.routers = [TestRouter()] + query = Person.objects.select_for_update() + self.assertEqual(router.db_for_write(Person), query.db) + finally: + router.routers = old_routers diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index e6723eac9b..f07e28df99 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.test import TestCase diff --git a/tests/select_related_onetoone/tests.py b/tests/select_related_onetoone/tests.py index d8ba4d0484..3942b2d221 100644 --- a/tests/select_related_onetoone/tests.py +++ b/tests/select_related_onetoone/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import unittest diff --git a/tests/select_related_regress/tests.py b/tests/select_related_regress/tests.py index f6d21b2dd9..da47cb771f 100644 --- a/tests/select_related_regress/tests.py +++ b/tests/select_related_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.test import TestCase from django.utils import six diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py index bff7c53249..07c220c52e 100644 --- a/tests/serializers/tests.py +++ b/tests/serializers/tests.py @@ -1,6 +1,6 @@ -from __future__ import absolute_import, unicode_literals - # -*- coding: utf-8 -*- +from __future__ import unicode_literals + import json from datetime import datetime import unittest diff --git a/tests/serializers_regress/tests.py b/tests/serializers_regress/tests.py index 1751816cee..3173f73985 100644 --- a/tests/serializers_regress/tests.py +++ b/tests/serializers_regress/tests.py @@ -6,7 +6,7 @@ test case that is capable of testing the capabilities of the serializers. This includes all valid data values, plus forward, backwards and self references. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import decimal diff --git a/tests/servers/tests.py b/tests/servers/tests.py index be74afe820..1f02a88d5a 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -113,7 +113,7 @@ class LiveServerAddress(LiveServerBase): def test_test_test(self): # Intentionally empty method so that the test is picked up by the - # test runner and the overriden setUpClass() method is executed. + # test runner and the overridden setUpClass() method is executed. pass class LiveServerViews(LiveServerBase): diff --git a/tests/servers/urls.py b/tests/servers/urls.py index a857c45f95..03393c30ec 100644 --- a/tests/servers/urls.py +++ b/tests/servers/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from . import views diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 87ed4d4f5e..4031d09a58 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -163,7 +163,7 @@ class SettingsTests(TestCase): def test_override_settings_delete(self): """ - Allow deletion of a setting in an overriden settings set (#18824) + Allow deletion of a setting in an overridden settings set (#18824) """ previous_i18n = settings.USE_I18N with self.settings(USE_I18N=False): @@ -226,6 +226,27 @@ class TestComplexSettingOverride(TestCase): self.assertEqual('Overriding setting TEST_WARN can lead to unexpected behaviour.', str(w[-1].message)) +class UniqueSettngsTests(TestCase): + """ + Tests for the INSTALLED_APPS setting. + """ + settings_module = settings + + def setUp(self): + self._installed_apps = self.settings_module.INSTALLED_APPS + + def tearDown(self): + self.settings_module.INSTALLED_APPS = self._installed_apps + + def test_unique(self): + """ + An ImproperlyConfigured exception is raised if the INSTALLED_APPS contains + any duplicate strings. + """ + with self.assertRaises(ImproperlyConfigured): + self.settings_module.INSTALLED_APPS = ("myApp1", "myApp1", "myApp2", "myApp3") + + class TrailingSlashURLTests(TestCase): """ Tests for the MEDIA_URL and STATIC_URL settings. diff --git a/tests/signals/tests.py b/tests/signals/tests.py index 58f25c2868..1dfe72bf5f 100644 --- a/tests/signals/tests.py +++ b/tests/signals/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db.models import signals from django.dispatch import receiver diff --git a/tests/signals_regress/tests.py b/tests/signals_regress/tests.py index 8fb3ad5a48..2c9858efda 100644 --- a/tests/signals_regress/tests.py +++ b/tests/signals_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import models from django.test import TestCase diff --git a/tests/sites_framework/tests.py b/tests/sites_framework/tests.py index 8e664fd501..7860394aa2 100644 --- a/tests/sites_framework/tests.py +++ b/tests/sites_framework/tests.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf import settings from django.contrib.sites.models import Site from django.test import TestCase diff --git a/tests/staticfiles_tests/tests.py b/tests/staticfiles_tests/tests.py index 912dcffe83..7200037050 100644 --- a/tests/staticfiles_tests/tests.py +++ b/tests/staticfiles_tests/tests.py @@ -650,8 +650,7 @@ class TestServeDisabled(TestServeStatic): settings.DEBUG = False def test_disabled_serving(self): - six.assertRaisesRegex(self, ImproperlyConfigured, 'The staticfiles view ' - 'can only be used in debug mode ', self._response, 'test.txt') + self.assertFileNotFound('test.txt') class TestServeStaticWithDefaultURL(TestServeStatic, TestDefaults): @@ -774,3 +773,33 @@ class TestTemplateTag(StaticFilesTestCase): self.assertStaticRenders("does/not/exist.png", "/static/does/not/exist.png") self.assertStaticRenders("testfile.txt", "/static/testfile.txt") + + +class TestAppStaticStorage(TestCase): + def setUp(self): + # Creates a python module foo_module in a directory with non ascii + # characters + self.search_path = 'search_path_\xc3\xbc' + os.mkdir(self.search_path) + module_path = os.path.join(self.search_path, 'foo_module') + os.mkdir(module_path) + open(os.path.join(module_path, '__init__.py'), 'w') + sys.path.append(os.path.abspath(self.search_path)) + + def tearDown(self): + sys.path.remove(os.path.abspath(self.search_path)) + shutil.rmtree(self.search_path) + + def test_app_with_non_ascii_characters_in_path(self): + """ + Regression test for #18404 - Tests AppStaticStorage with a module that + has non ascii characters in path and a non utf8 file system encoding + """ + # set file system encoding to a non unicode encoding + old_enc_func = sys.getfilesystemencoding + sys.getfilesystemencoding = lambda: 'ISO-8859-1' + try: + st = storage.AppStaticStorage('foo_module') + st.path('bar') + finally: + sys.getfilesystemencoding = old_enc_func diff --git a/tests/str/tests.py b/tests/str/tests.py index d82908a0ee..3c0bc079c8 100644 --- a/tests/str/tests.py +++ b/tests/str/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime from unittest import skipIf diff --git a/tests/string_lookup/tests.py b/tests/string_lookup/tests.py index b011720ddf..5a17e55560 100644 --- a/tests/string_lookup/tests.py +++ b/tests/string_lookup/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.test import TestCase from .models import Foo, Whiz, Bar, Article, Base, Child @@ -80,4 +80,4 @@ class StringLookupTests(TestCase): self.assertEqual(repr(Article.objects.filter(submitted_from__contains='192.0.2')), repr([a])) # Test that the searches do not match the subnet mask (/32 in this case) - self.assertEqual(Article.objects.filter(submitted_from__contains='32').count(), 0)
\ No newline at end of file + self.assertEqual(Article.objects.filter(submitted_from__contains='32').count(), 0) diff --git a/tests/swappable_models/tests.py b/tests/swappable_models/tests.py index 858061db23..2e2d544cea 100644 --- a/tests/swappable_models/tests.py +++ b/tests/swappable_models/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.utils.six import StringIO diff --git a/tests/syndication/feeds.py b/tests/syndication/feeds.py index 1cd5c3d988..f8ffb4b2e6 100644 --- a/tests/syndication/feeds.py +++ b/tests/syndication/feeds.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.syndication import views from django.core.exceptions import ObjectDoesNotExist diff --git a/tests/syndication/tests.py b/tests/syndication/tests.py index d3b0058d53..8bc6b04939 100644 --- a/tests/syndication/tests.py +++ b/tests/syndication/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from xml.dom import minidom diff --git a/tests/syndication/urls.py b/tests/syndication/urls.py index 06a75a4e68..1b5d77f2e1 100644 --- a/tests/syndication/urls.py +++ b/tests/syndication/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns from . import feeds diff --git a/tests/tablespaces/tests.py b/tests/tablespaces/tests.py index 1eaddef079..088938ca1d 100644 --- a/tests/tablespaces/tests.py +++ b/tests/tablespaces/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import copy diff --git a/tests/template_tests/alternate_urls.py b/tests/template_tests/alternate_urls.py index fa4985a9dc..3c6a4e62f9 100644 --- a/tests/template_tests/alternate_urls.py +++ b/tests/template_tests/alternate_urls.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import absolute_import - from django.conf.urls import patterns, url from . import views diff --git a/tests/template_tests/test_custom.py b/tests/template_tests/test_custom.py index e941bc223e..c2dff03b10 100644 --- a/tests/template_tests/test_custom.py +++ b/tests/template_tests/test_custom.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from unittest import TestCase diff --git a/tests/template_tests/loaders.py b/tests/template_tests/test_loaders.py index 497b422e7f..6fd17fd04b 100644 --- a/tests/template_tests/loaders.py +++ b/tests/template_tests/test_loaders.py @@ -11,10 +11,15 @@ if __name__ == '__main__': import imp import os.path -import pkg_resources import sys import unittest +try: + import pkg_resources +except ImportError: + pkg_resources = None + + from django.template import TemplateDoesNotExist, Context from django.template.loaders.eggs import Loader as EggLoader from django.template import loader @@ -24,23 +29,6 @@ from django.utils.six import StringIO # Mock classes and objects for pkg_resources functions. -class MockProvider(pkg_resources.NullProvider): - def __init__(self, module): - pkg_resources.NullProvider.__init__(self, module) - self.module = module - - def _has(self, path): - return path in self.module._resources - - def _isdir(self, path): - return False - - def get_resource_stream(self, manager, resource_name): - return self.module._resources[resource_name] - - def _get(self, path): - return self.module._resources[path].read() - class MockLoader(object): pass @@ -57,8 +45,27 @@ def create_egg(name, resources): sys.modules[name] = egg +@unittest.skipUnless(pkg_resources, 'setuptools is not installed') class EggLoaderTest(unittest.TestCase): def setUp(self): + # Defined here b/c at module scope we may not have pkg_resources + class MockProvider(pkg_resources.NullProvider): + def __init__(self, module): + pkg_resources.NullProvider.__init__(self, module) + self.module = module + + def _has(self, path): + return path in self.module._resources + + def _isdir(self, path): + return False + + def get_resource_stream(self, manager, resource_name): + return self.module._resources[resource_name] + + def _get(self, path): + return self.module._resources[path].read() + pkg_resources._provider_factories[MockLoader] = MockProvider self.empty_egg = create_egg("egg_empty", {}) diff --git a/tests/template_tests/test_response.py b/tests/template_tests/test_response.py index 6acc45626f..65f7531361 100644 --- a/tests/template_tests/test_response.py +++ b/tests/template_tests/test_response.py @@ -95,7 +95,7 @@ class SimpleTemplateResponseTest(TestCase): self.assertEqual(response.content, b'foo') def test_set_content(self): - # content can be overriden + # content can be overridden response = self._response() self.assertFalse(response.is_rendered) response.content = 'spam' diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 6b8a106623..aa0283be79 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf import settings @@ -8,8 +8,7 @@ if __name__ == '__main__': # before importing 'template'. settings.configure() -from datetime import date, datetime, timedelta -import time +from datetime import date, datetime import os import sys import traceback @@ -31,21 +30,12 @@ from django.test.utils import (setup_test_template_loader, from django.utils.encoding import python_2_unicode_compatible from django.utils.formats import date_format from django.utils._os import upath -from django.utils.translation import activate, deactivate, ugettext as _ +from django.utils.translation import activate, deactivate from django.utils.safestring import mark_safe from django.utils import six -from django.utils.tzinfo import LocalTimezone from i18n import TransRealMixin -try: - from .loaders import RenderToStringTest, EggLoaderTest -except ImportError as e: - if "pkg_resources" in e.args[0]: - pass # If setuptools isn't installed, that's fine. Just move on. - else: - raise - # NumPy installed? try: import numpy diff --git a/tests/template_tests/urls.py b/tests/template_tests/urls.py index b5498fade1..f3720bbda7 100644 --- a/tests/template_tests/urls.py +++ b/tests/template_tests/urls.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf.urls import patterns, url from . import views diff --git a/tests/templates/custom_admin/app_index.html b/tests/templates/custom_admin/app_index.html new file mode 100644 index 0000000000..3dfae814a7 --- /dev/null +++ b/tests/templates/custom_admin/app_index.html @@ -0,0 +1,6 @@ +{% extends "admin/app_index.html" %} + +{% block content %} +Hello from a custom app_index template +{{ block.super }} +{% endblock %} diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 0f3cba7e88..85637b982e 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -20,7 +20,7 @@ testing against the contexts and templates produced by a view, rather than the HTML rendered to the end-user. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf import settings from django.core import mail diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index bd395ca552..4d2f4fb86e 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns from django.views.generic import RedirectView diff --git a/tests/test_client_regress/urls.py b/tests/test_client_regress/urls.py index 1332537d57..6a0b330e02 100644 --- a/tests/test_client_regress/urls.py +++ b/tests/test_client_regress/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from django.views.generic import RedirectView diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 72200e40d3..7aefa4e077 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -1,8 +1,9 @@ """ Tests for django test runner """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals +from importlib import import_module from optparse import make_option import sys import unittest @@ -13,7 +14,6 @@ from django import db from django.test import runner, TestCase, TransactionTestCase, skipUnlessDBFeature from django.test.testcases import connections_support_transactions from django.test.utils import IgnoreAllDeprecationWarningsMixin -from django.utils.importlib import import_module from admin_scripts.tests import AdminScriptTestCase from .models import Person diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 24aa433dd3..eb850cc3be 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import unittest diff --git a/tests/test_utils/urls.py b/tests/test_utils/urls.py index 31fc5cc7fc..65e8631735 100644 --- a/tests/test_utils/urls.py +++ b/tests/test_utils/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns from . import views diff --git a/tests/test_utils/views.py b/tests/test_utils/views.py index 5495488e2c..77e598b72c 100644 --- a/tests/test_utils/views.py +++ b/tests/test_utils/views.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.http import HttpResponse from django.shortcuts import get_object_or_404 @@ -8,4 +6,4 @@ from .models import Person def get_person(request, pk): person = get_object_or_404(Person, pk=pk) - return HttpResponse(person.name)
\ No newline at end of file + return HttpResponse(person.name) diff --git a/tests/timezones/admin.py b/tests/timezones/admin.py index 4c199813e2..81b49a4ab6 100644 --- a/tests/timezones/admin.py +++ b/tests/timezones/admin.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.contrib import admin from .models import Event, Timestamp diff --git a/tests/timezones/urls.py b/tests/timezones/urls.py index e4f42972db..e9a7a90df9 100644 --- a/tests/timezones/urls.py +++ b/tests/timezones/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, include from django.contrib import admin diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index afb573f366..9cf8b4d742 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import sys from unittest import skipIf, skipUnless diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 01f8fc4186..2376bcd277 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from unittest import skipIf, skipUnless diff --git a/tests/unmanaged_models/tests.py b/tests/unmanaged_models/tests.py index d7cf961a37..7b23a46096 100644 --- a/tests/unmanaged_models/tests.py +++ b/tests/unmanaged_models/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db import connection from django.test import TestCase diff --git a/tests/update/tests.py b/tests/update/tests.py index 7a1177c1fd..95e9069972 100644 --- a/tests/update/tests.py +++ b/tests/update/tests.py @@ -1,8 +1,8 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.test import TestCase -from .models import A, B, C, D, DataPoint, RelatedPoint +from .models import A, B, D, DataPoint, RelatedPoint class SimpleTest(TestCase): @@ -51,6 +51,15 @@ class SimpleTest(TestCase): cnt = D.objects.filter(y=100).count() self.assertEqual(cnt, 0) + def test_foreign_key_update_with_id(self): + """ + Test that update works using <field>_id for foreign keys + """ + num_updated = self.a1.d_set.update(a_id=self.a2) + self.assertEqual(num_updated, 20) + self.assertEqual(self.a2.d_set.count(), 20) + + class AdvancedTests(TestCase): def setUp(self): @@ -115,4 +124,4 @@ class AdvancedTests(TestCase): """ method = DataPoint.objects.all()[:2].update self.assertRaises(AssertionError, method, - another_value='another thing') + another_value='another thing') diff --git a/tests/update_only_fields/tests.py b/tests/update_only_fields/tests.py index 97c05ddc79..1f85c3bbb2 100644 --- a/tests/update_only_fields/tests.py +++ b/tests/update_only_fields/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals from django.db.models.signals import pre_save, post_save from django.test import TestCase diff --git a/tests/urlpatterns_reverse/extra_urls.py b/tests/urlpatterns_reverse/extra_urls.py index d3a04e6b31..94e225fb46 100644 --- a/tests/urlpatterns_reverse/extra_urls.py +++ b/tests/urlpatterns_reverse/extra_urls.py @@ -2,8 +2,6 @@ Some extra URL patterns that are included at the top level. """ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .views import empty_view diff --git a/tests/urlpatterns_reverse/included_named_urls.py b/tests/urlpatterns_reverse/included_named_urls.py index 353aed255e..366fe9b57a 100644 --- a/tests/urlpatterns_reverse/included_named_urls.py +++ b/tests/urlpatterns_reverse/included_named_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .views import empty_view diff --git a/tests/urlpatterns_reverse/included_named_urls2.py b/tests/urlpatterns_reverse/included_named_urls2.py index b31bdb1f7e..b8e4c531fa 100644 --- a/tests/urlpatterns_reverse/included_named_urls2.py +++ b/tests/urlpatterns_reverse/included_named_urls2.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from .views import empty_view diff --git a/tests/urlpatterns_reverse/included_namespace_urls.py b/tests/urlpatterns_reverse/included_namespace_urls.py index 7a2096ecd9..ae098a64dc 100644 --- a/tests/urlpatterns_reverse/included_namespace_urls.py +++ b/tests/urlpatterns_reverse/included_namespace_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .namespace_urls import URLObject diff --git a/tests/urlpatterns_reverse/included_urls.py b/tests/urlpatterns_reverse/included_urls.py index c8c9001843..af6b6882e0 100644 --- a/tests/urlpatterns_reverse/included_urls.py +++ b/tests/urlpatterns_reverse/included_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from .views import empty_view diff --git a/tests/urlpatterns_reverse/included_urls2.py b/tests/urlpatterns_reverse/included_urls2.py index 98605047f4..9dcafc8535 100644 --- a/tests/urlpatterns_reverse/included_urls2.py +++ b/tests/urlpatterns_reverse/included_urls2.py @@ -5,8 +5,6 @@ each name to resolve and Django must distinguish the possibilities based on the argument list. """ -from __future__ import absolute_import - from django.conf.urls import patterns, url from .views import empty_view diff --git a/tests/urlpatterns_reverse/middleware.py b/tests/urlpatterns_reverse/middleware.py index fbf577786e..0de692835f 100644 --- a/tests/urlpatterns_reverse/middleware.py +++ b/tests/urlpatterns_reverse/middleware.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.core.urlresolvers import reverse from django.http import HttpResponse, StreamingHttpResponse diff --git a/tests/urlpatterns_reverse/named_urls.py b/tests/urlpatterns_reverse/named_urls.py index 3290cab29c..d4d35bac33 100644 --- a/tests/urlpatterns_reverse/named_urls.py +++ b/tests/urlpatterns_reverse/named_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .views import empty_view diff --git a/tests/urlpatterns_reverse/namespace_urls.py b/tests/urlpatterns_reverse/namespace_urls.py index cf960070ec..014bc65990 100644 --- a/tests/urlpatterns_reverse/namespace_urls.py +++ b/tests/urlpatterns_reverse/namespace_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .views import view_class_instance diff --git a/tests/urlpatterns_reverse/reverse_lazy_urls.py b/tests/urlpatterns_reverse/reverse_lazy_urls.py index 693c6e1b38..0ef0a1f313 100644 --- a/tests/urlpatterns_reverse/reverse_lazy_urls.py +++ b/tests/urlpatterns_reverse/reverse_lazy_urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url from .views import empty_view, LazyRedirectView, login_required_view diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 222ebe053b..aef0fa0514 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -1,7 +1,7 @@ """ Unit tests for reverse URL lookups. """ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import unittest diff --git a/tests/urlpatterns_reverse/urlconf_outer.py b/tests/urlpatterns_reverse/urlconf_outer.py index 0cdebf83ff..20b9b09f98 100644 --- a/tests/urlpatterns_reverse/urlconf_outer.py +++ b/tests/urlpatterns_reverse/urlconf_outer.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from . import urlconf_inner @@ -8,4 +6,4 @@ from . import urlconf_inner urlpatterns = patterns('', url(r'^test/me/$', urlconf_inner.inner_view, name='outer'), url(r'^inner_urlconf/', include(urlconf_inner.__name__)) -)
\ No newline at end of file +) diff --git a/tests/urlpatterns_reverse/urls.py b/tests/urlpatterns_reverse/urls.py index 1dbc8d889f..cb08e2e664 100644 --- a/tests/urlpatterns_reverse/urls.py +++ b/tests/urlpatterns_reverse/urls.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django.conf.urls import patterns, url, include from .views import empty_view, absolute_kwargs_view diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py index befeccaf45..0900ec94ba 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py +++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -1,7 +1,5 @@ # Used by the ErrorHandlerResolutionTests test case. -from __future__ import absolute_import - from django.conf.urls import patterns from .views import empty_view diff --git a/tests/urlpatterns_reverse/urls_without_full_import.py b/tests/urlpatterns_reverse/urls_without_full_import.py index ca3e424f23..c1dce5549c 100644 --- a/tests/urlpatterns_reverse/urls_without_full_import.py +++ b/tests/urlpatterns_reverse/urls_without_full_import.py @@ -1,8 +1,6 @@ # A URLs file that doesn't use the default # from django.conf.urls import * # import pattern. -from __future__ import absolute_import - from django.conf.urls import patterns, url from .views import empty_view, bad_view diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index 1f2e19d8d5..74d94ea19d 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime @@ -181,3 +182,13 @@ class TestUtilsHtml(TestCase): ) for value, tags, output in items: self.assertEqual(f(value, tags), output) + + def test_smart_urlquote(self): + quote = html.smart_urlquote + # Ensure that IDNs are properly quoted + self.assertEqual(quote('http://öäü.com/'), 'http://xn--4ca9at.com/') + self.assertEqual(quote('http://öäü.com/öäü/'), 'http://xn--4ca9at.com/%C3%B6%C3%A4%C3%BC/') + # Ensure that everything unsafe is quoted, !*'();:@&=+$,/?#[]~ is considered safe as per RFC + self.assertEqual(quote('http://example.com/path/öäü/'), 'http://example.com/path/%C3%B6%C3%A4%C3%BC/') + self.assertEqual(quote('http://example.com/%C3%B6/ä/'), 'http://example.com/%C3%B6/%C3%A4/') + self.assertEqual(quote('http://example.com/?x=1&y=2'), 'http://example.com/?x=1&y=2') diff --git a/tests/utils_tests/test_module_loading.py b/tests/utils_tests/test_module_loading.py index 5a43a927bf..2423215778 100644 --- a/tests/utils_tests/test_module_loading.py +++ b/tests/utils_tests/test_module_loading.py @@ -1,11 +1,11 @@ import imp +from importlib import import_module import os import sys import unittest from zipimport import zipimporter from django.core.exceptions import ImproperlyConfigured -from django.utils.importlib import import_module from django.utils.module_loading import import_by_path, module_has_submodule from django.utils._os import upath diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py index a6f0c0a01c..5d4528a9a8 100644 --- a/tests/utils_tests/test_safestring.py +++ b/tests/utils_tests/test_safestring.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import, unicode_literals - +from __future__ import unicode_literals from django.template import Template, Context from django.test import TestCase diff --git a/tests/validation/test_custom_messages.py b/tests/validation/test_custom_messages.py index c5a1ee744f..2e259b7aef 100644 --- a/tests/validation/test_custom_messages.py +++ b/tests/validation/test_custom_messages.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from . import ValidationTestCase from .models import CustomMessagesModel diff --git a/tests/validation/test_unique.py b/tests/validation/test_unique.py index a481fcb1c4..bca1b36c93 100644 --- a/tests/validation/test_unique.py +++ b/tests/validation/test_unique.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import datetime import unittest diff --git a/tests/validation/test_validators.py b/tests/validation/test_validators.py index e58d9fd4a6..c3875d4601 100644 --- a/tests/validation/test_validators.py +++ b/tests/validation/test_validators.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from . import ValidationTestCase from .models import ModelToValidate diff --git a/tests/validation/tests.py b/tests/validation/tests.py index c8b679541a..34cde3fc8a 100644 --- a/tests/validation/tests.py +++ b/tests/validation/tests.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django import forms from django.core.exceptions import NON_FIELD_ERRORS diff --git a/tests/view_tests/generic_urls.py b/tests/view_tests/generic_urls.py index c3ac1fcafa..10e7601eb6 100644 --- a/tests/view_tests/generic_urls.py +++ b/tests/view_tests/generic_urls.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.conf.urls import patterns, url from django.views.generic import RedirectView diff --git a/tests/view_tests/templatetags/debugtags.py b/tests/view_tests/templatetags/debugtags.py index cd2d2d9ad2..9e1945cb23 100644 --- a/tests/view_tests/templatetags/debugtags.py +++ b/tests/view_tests/templatetags/debugtags.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from django import template from ..views import BrokenException diff --git a/tests/view_tests/tests/__init__.py b/tests/view_tests/tests/__init__.py index bfb2ed7376..dae149a8ef 100644 --- a/tests/view_tests/tests/__init__.py +++ b/tests/view_tests/tests/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from .test_debug import (DebugViewTests, ExceptionReporterTests, ExceptionReporterTests, PlainTextReportTests, ExceptionReporterFilterTests, AjaxResponseExceptionReporterFilter) diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index bc77fc351a..0159886918 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # This coding header is significant for tests, as the debug view is parsing # files to search for such a header to decode the source file content -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals import inspect import os diff --git a/tests/view_tests/tests/test_defaults.py b/tests/view_tests/tests/test_defaults.py index 5efd338d34..d55ed53454 100644 --- a/tests/view_tests/tests/test_defaults.py +++ b/tests/view_tests/tests/test_defaults.py @@ -1,8 +1,9 @@ -from __future__ import absolute_import, unicode_literals +from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from django.test.utils import setup_test_template_loader, restore_template_loaders +from django.test.utils import (setup_test_template_loader, + restore_template_loaders, override_settings) from ..models import Author, Article, UrlArticle @@ -59,3 +60,20 @@ class DefaultsTests(TestCase): article = UrlArticle.objects.get(pk=1) self.assertTrue(getattr(article.get_absolute_url, 'purge', False), 'The attributes of the original get_absolute_url must be added.') + + @override_settings(DEFAULT_CONTENT_TYPE="text/xml") + def test_default_content_type_is_text_html(self): + """ + Content-Type of the default error responses is text/html. Refs #20822. + """ + response = self.client.get('/views/raises400/') + self.assertEqual(response['Content-Type'], 'text/html') + + response = self.client.get('/views/raises403/') + self.assertEqual(response['Content-Type'], 'text/html') + + response = self.client.get('/views/non_existing_url/') + self.assertEqual(response['Content-Type'], 'text/html') + + response = self.client.get('/views/server_error/') + self.assertEqual(response['Content-Type'], 'text/html') diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index c1852ee71f..cfee4f5093 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -1,6 +1,4 @@ # -*- coding:utf-8 -*- -from __future__ import absolute_import - import gettext import os from os import path @@ -200,26 +198,22 @@ class JavascriptI18nTests(LiveServerTestCase): @override_settings(LANGUAGE_CODE='de') def test_javascript_gettext(self): - extended_apps = list(settings.INSTALLED_APPS) + ['view_tests'] - with self.settings(INSTALLED_APPS=extended_apps): - self.selenium.get('%s%s' % (self.live_server_url, '/jsi18n_template/')) + self.selenium.get('%s%s' % (self.live_server_url, '/jsi18n_template/')) - elem = self.selenium.find_element_by_id("gettext") - self.assertEqual(elem.text, "Entfernen") - elem = self.selenium.find_element_by_id("ngettext_sing") - self.assertEqual(elem.text, "1 Element") - elem = self.selenium.find_element_by_id("ngettext_plur") - self.assertEqual(elem.text, "455 Elemente") - elem = self.selenium.find_element_by_id("pgettext") - self.assertEqual(elem.text, "Kann") - elem = self.selenium.find_element_by_id("npgettext_sing") - self.assertEqual(elem.text, "1 Resultat") - elem = self.selenium.find_element_by_id("npgettext_plur") - self.assertEqual(elem.text, "455 Resultate") + elem = self.selenium.find_element_by_id("gettext") + self.assertEqual(elem.text, "Entfernen") + elem = self.selenium.find_element_by_id("ngettext_sing") + self.assertEqual(elem.text, "1 Element") + elem = self.selenium.find_element_by_id("ngettext_plur") + self.assertEqual(elem.text, "455 Elemente") + elem = self.selenium.find_element_by_id("pgettext") + self.assertEqual(elem.text, "Kann") + elem = self.selenium.find_element_by_id("npgettext_sing") + self.assertEqual(elem.text, "1 Resultat") + elem = self.selenium.find_element_by_id("npgettext_plur") + self.assertEqual(elem.text, "455 Resultate") def test_escaping(self): - extended_apps = list(settings.INSTALLED_APPS) + ['view_tests'] - with self.settings(INSTALLED_APPS=extended_apps): - # Force a language via GET otherwise the gettext functions are a noop! - response = self.client.get('/jsi18n_admin/?language=de') - self.assertContains(response, '\\x04') + # Force a language via GET otherwise the gettext functions are a noop! + response = self.client.get('/jsi18n_admin/?language=de') + self.assertContains(response, '\\x04') diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py index 6104ad063e..d2f3f47fa7 100644 --- a/tests/view_tests/tests/test_static.py +++ b/tests/view_tests/tests/test_static.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import mimetypes from os import path diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index d792e47ddf..f2c910368d 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -1,6 +1,4 @@ # coding: utf-8 -from __future__ import absolute_import - from os import path from django.conf.urls import patterns, url, include @@ -47,8 +45,10 @@ urlpatterns = patterns('', # a view that raises an exception for the debug view (r'raises/$', views.raises), - (r'raises404/$', views.raises404), + + (r'raises400/$', views.raises400), (r'raises403/$', views.raises403), + (r'raises404/$', views.raises404), # i18n views (r'^i18n/', include('django.conf.urls.i18n')), diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 1cfafa4333..0bac7d9321 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -1,8 +1,8 @@ -from __future__ import absolute_import +from __future__ import unicode_literals import sys -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.urlresolvers import get_resolver from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render_to_response, render @@ -31,13 +31,16 @@ def raises(request): except Exception: return technical_500_response(request, *sys.exc_info()) -def raises404(request): - resolver = get_resolver(None) - resolver.resolve('') +def raises400(request): + raise SuspiciousOperation def raises403(request): raise PermissionDenied +def raises404(request): + resolver = get_resolver(None) + resolver.resolve('') + def redirect(request): """ Forces an HTTP redirect. |
