diff --git a/AUTHORS b/AUTHORS index a7f12d6e48..15f3e8dbf4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better: Paul Collier Paul Collins Robert Coup + Alex Couper Deric Crago Brian Fabian Crain David Cramer @@ -416,6 +417,7 @@ answer newbie questions, and generally made Django that much better: Zain Memon Christian Metts michal@plovarna.cz + Justin Michalicek Slawek Mikula Katie Miller Shawn Milochik @@ -542,6 +544,7 @@ answer newbie questions, and generally made Django that much better: smurf@smurf.noris.de Vsevolod Solovyov George Song + Jimmy Song sopel Leo Soto 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 deleted file mode 100644 index 30e83c2518..0000000000 Binary files a/django/contrib/admin/static/admin/img/chooser-bg.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif b/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif deleted file mode 100644 index 5d104b6d98..0000000000 Binary files a/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tool-left.gif b/django/contrib/admin/static/admin/img/tool-left.gif deleted file mode 100644 index 011490ff3a..0000000000 Binary files a/django/contrib/admin/static/admin/img/tool-left.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tool-left_over.gif b/django/contrib/admin/static/admin/img/tool-left_over.gif deleted file mode 100644 index 937e07bb1a..0000000000 Binary files a/django/contrib/admin/static/admin/img/tool-left_over.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tool-right.gif b/django/contrib/admin/static/admin/img/tool-right.gif deleted file mode 100644 index cdc140cc59..0000000000 Binary files a/django/contrib/admin/static/admin/img/tool-right.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tool-right_over.gif b/django/contrib/admin/static/admin/img/tool-right_over.gif deleted file mode 100644 index 4db977e838..0000000000 Binary files a/django/contrib/admin/static/admin/img/tool-right_over.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tooltag-add.gif b/django/contrib/admin/static/admin/img/tooltag-add.gif deleted file mode 100644 index 8b53d49ae5..0000000000 Binary files a/django/contrib/admin/static/admin/img/tooltag-add.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tooltag-add.png b/django/contrib/admin/static/admin/img/tooltag-add.png new file mode 100644 index 0000000000..f352cf590f Binary files /dev/null and b/django/contrib/admin/static/admin/img/tooltag-add.png differ diff --git a/django/contrib/admin/static/admin/img/tooltag-add_over.gif b/django/contrib/admin/static/admin/img/tooltag-add_over.gif deleted file mode 100644 index bfc52f10de..0000000000 Binary files a/django/contrib/admin/static/admin/img/tooltag-add_over.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright.gif b/django/contrib/admin/static/admin/img/tooltag-arrowright.gif deleted file mode 100644 index cdaaae77ed..0000000000 Binary files a/django/contrib/admin/static/admin/img/tooltag-arrowright.gif and /dev/null differ diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright.png b/django/contrib/admin/static/admin/img/tooltag-arrowright.png new file mode 100644 index 0000000000..ac1ac5b89a Binary files /dev/null and b/django/contrib/admin/static/admin/img/tooltag-arrowright.png differ diff --git a/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif b/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif deleted file mode 100644 index 7163189604..0000000000 Binary files a/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif and /dev/null differ 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(' ('+gettext("Show")+")")});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(' ('+gettext("Show")+")")});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'+a.addText+""),h=d.find("tr:last a")):(c.filter(":last").after('"),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('"):c.is("ul")||c.is("ol")?c.append('
  • '+a.deleteText+"
  • "):c.children(":first").append(''+a.deleteText+"");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;c0;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(''+b.addText+"");f=g.find("tr:last a")}else{i.filter(":last").after('");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('");else h.is("ul")||h.is("ol")?h.append('
  • '+b.deleteText+"
  • "):h.children(":first").append(''+b.deleteText+""); +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 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 @@ {% endblock %} -{% block title %}{% trans 'Password change successful' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %} - -

    {% trans 'Password change successful' %}

    -

    {% trans 'Your password was changed.' %}

    - {% 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 @@ {% endblock %} -{% block title %}{% trans 'Password change' %}{% endblock %} +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %}
    @@ -21,7 +22,6 @@

    {% endif %} -

    {% trans 'Password change' %}

    {% 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." %}

    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 @@
    {% endblock %} -{% block title %}{% trans 'Password reset complete' %}{% endblock %} +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %} -

    {% trans 'Password reset complete' %}

    -

    {% trans "Your password has been set. You may go ahead and log in now." %}

    {% trans 'Log in' %}

    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 @@ {% endblock %} -{% block title %}{% trans 'Password reset' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %} {% if validlink %} -

    {% trans 'Enter new password' %}

    -

    {% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

    {% csrf_token %} @@ -28,8 +26,6 @@ {% else %} -

    {% trans 'Password reset unsuccessful' %}

    -

    {% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

    {% 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 @@ {% endblock %} -{% block title %}{% trans 'Password reset successful' %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %} -

    {% trans 'Password reset successful' %}

    -

    {% trans "We've emailed you instructions for setting your password. You should be receiving them shortly." %}

    {% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}

    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 @@ {% endblock %} -{% block title %}{% trans "Password reset" %}{% endblock %} - +{% block title %}{{ title }}{% endblock %} +{% block content_title %}

    {{ title }}

    {% endblock %} {% block content %} -

    {% trans "Password reset" %}

    -

    {% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

    {% 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 @@ +Link 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 # 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'^Link$', 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, 'Password reset') + self.assertContains(response, '

    Password reset

    ') + + response = password_reset_done(request) + self.assertContains(response, 'Password reset successful') + self.assertContains(response, '

    Password reset successful

    ') + + # password_reset_confirm invalid token + response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/') + self.assertContains(response, 'Password reset unsuccessful') + self.assertContains(response, '

    Password reset unsuccessful

    ') + + # 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, 'Enter new password') + self.assertContains(response, '

    Enter new password

    ') + + response = password_reset_complete(request) + self.assertContains(response, 'Password reset complete') + self.assertContains(response, '

    Password reset complete

    ') + + response = password_change(request, post_change_redirect='dummy/') + self.assertContains(response, 'Password change') + self.assertContains(response, '

    Password change

    ') + + response = password_change_done(request) + self.assertContains(response, 'Password change successful') + self.assertContains(response, '

    Password change successful

    ') 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('' not in message.get_payload(0).get_payload()) + self.assertTrue('' 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' + 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): - # Make me a superuser before loging in. - User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True) - self.login() - # 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[0-9A-Za-z_\-]+)/(?P[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 '/models/sql' is " + "deprecated, use '/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