summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2013-08-09 14:17:30 +0100
committerAndrew Godwin <andrew@aeracode.org>2013-08-09 14:17:30 +0100
commitde64c4d6e97c980fb4c0ace045fc4070b3f763d9 (patch)
tree045c8af6a88fa964cf7e284970a6e93d53941a79 /django
parentfddc5957c53bd654312c4a238a8cdcfe5f4ef4cc (diff)
parentb575d690bbc1c4cd7f575346132c09fca8c736a7 (diff)
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/core/management/commands/flush.py django/core/management/commands/syncdb.py django/db/models/loading.py docs/internals/deprecation.txt docs/ref/django-admin.txt docs/releases/1.7.txt
Diffstat (limited to 'django')
-rw-r--r--django/conf/__init__.py5
-rw-r--r--django/conf/urls/__init__.py3
-rw-r--r--django/contrib/admin/__init__.py2
-rw-r--r--django/contrib/admin/filters.py2
-rw-r--r--django/contrib/admin/helpers.py8
-rw-r--r--django/contrib/admin/models.py7
-rw-r--r--django/contrib/admin/options.py217
-rw-r--r--django/contrib/admin/static/admin/css/base.css32
-rw-r--r--django/contrib/admin/static/admin/css/widgets.css8
-rw-r--r--django/contrib/admin/static/admin/img/chooser-bg.gifbin199 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/chooser_stacked-bg.gifbin212 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tool-left.gifbin197 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tool-left_over.gifbin203 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tool-right.gifbin198 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tool-right_over.gifbin200 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-add.gifbin932 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-add.pngbin0 -> 967 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-add_over.gifbin336 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-arrowright.gifbin351 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-arrowright.pngbin0 -> 1063 bytes
-rw-r--r--django/contrib/admin/static/admin/img/tooltag-arrowright_over.gifbin354 -> 0 bytes
-rw-r--r--django/contrib/admin/static/admin/js/collapse.min.js4
-rw-r--r--django/contrib/admin/static/admin/js/inlines.min.js18
-rw-r--r--django/contrib/admin/static/admin/js/prepopulate.js41
-rw-r--r--django/contrib/admin/static/admin/js/prepopulate.min.js2
-rw-r--r--django/contrib/admin/templates/registration/password_change_done.html8
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html4
-rw-r--r--django/contrib/admin/templates/registration/password_reset_complete.html5
-rw-r--r--django/contrib/admin/templates/registration/password_reset_confirm.html8
-rw-r--r--django/contrib/admin/templates/registration/password_reset_done.html6
-rw-r--r--django/contrib/admin/templates/registration/password_reset_form.html6
-rw-r--r--django/contrib/admin/templatetags/admin_list.py9
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py2
-rw-r--r--django/contrib/admin/views/main.py6
-rw-r--r--django/contrib/admin/widgets.py11
-rw-r--r--django/contrib/admindocs/tests/test_fields.py2
-rw-r--r--django/contrib/admindocs/views.py4
-rw-r--r--django/contrib/auth/admin.py7
-rw-r--r--django/contrib/auth/backends.py4
-rw-r--r--django/contrib/auth/forms.py47
-rw-r--r--django/contrib/auth/hashers.py20
-rw-r--r--django/contrib/auth/tests/templates/registration/html_password_reset_email.html1
-rw-r--r--django/contrib/auth/tests/test_auth_backends.py29
-rw-r--r--django/contrib/auth/tests/test_context_processors.py2
-rw-r--r--django/contrib/auth/tests/test_forms.py92
-rw-r--r--django/contrib/auth/tests/test_templates.py59
-rw-r--r--django/contrib/auth/tests/test_views.py93
-rw-r--r--django/contrib/auth/tests/urls.py1
-rw-r--r--django/contrib/auth/views.py23
-rw-r--r--django/contrib/comments/__init__.py2
-rw-r--r--django/contrib/comments/views/comments.py2
-rw-r--r--django/contrib/comments/views/moderation.py2
-rw-r--r--django/contrib/contenttypes/generic.py6
-rw-r--r--django/contrib/flatpages/tests/test_csrf.py1
-rw-r--r--django/contrib/formtools/preview.py2
-rw-r--r--django/contrib/formtools/tests/urls.py2
-rw-r--r--django/contrib/formtools/tests/wizard/storage.py2
-rw-r--r--django/contrib/formtools/tests/wizard/test_forms.py3
-rw-r--r--django/contrib/formtools/tests/wizard/wizardtests/forms.py3
-rw-r--r--django/contrib/formtools/wizard/views.py22
-rw-r--r--django/contrib/gis/admin/options.py2
-rw-r--r--django/contrib/gis/db/backends/spatialite/creation.py6
-rw-r--r--django/contrib/gis/db/models/fields.py1
-rw-r--r--django/contrib/gis/gdal/geometries.py4
-rw-r--r--django/contrib/gis/gdal/tests/test_envelope.py6
-rw-r--r--django/contrib/gis/gdal/tests/test_geom.py21
-rw-r--r--django/contrib/gis/gdal/tests/test_srs.py4
-rw-r--r--django/contrib/gis/geoip/__init__.py2
-rw-r--r--django/contrib/gis/geoip/base.py3
-rw-r--r--django/contrib/gis/geoip/prototypes.py2
-rw-r--r--django/contrib/gis/geometry/backend/__init__.py5
-rw-r--r--django/contrib/gis/geos/geometry.py1
-rw-r--r--django/contrib/gis/geos/tests/test_geos.py18
-rw-r--r--django/contrib/gis/tests/distapp/tests.py2
-rw-r--r--django/contrib/gis/tests/geo3d/tests.py2
-rw-r--r--django/contrib/gis/tests/geoadmin/tests.py2
-rw-r--r--django/contrib/gis/tests/geoapp/feeds.py2
-rw-r--r--django/contrib/gis/tests/geoapp/sitemaps.py2
-rw-r--r--django/contrib/gis/tests/geoapp/test_feeds.py2
-rw-r--r--django/contrib/gis/tests/geoapp/test_regress.py2
-rw-r--r--django/contrib/gis/tests/geoapp/test_sitemaps.py2
-rw-r--r--django/contrib/gis/tests/geoapp/tests.py2
-rw-r--r--django/contrib/gis/tests/geoapp/urls.py2
-rw-r--r--django/contrib/gis/tests/geogapp/tests.py2
-rw-r--r--django/contrib/gis/tests/inspectapp/tests.py2
-rw-r--r--django/contrib/gis/tests/layermap/tests.py2
-rw-r--r--django/contrib/gis/tests/relatedapp/tests.py2
-rw-r--r--django/contrib/gis/tests/test_spatialrefsys.py2
-rw-r--r--django/contrib/messages/__init__.py2
-rw-r--r--django/contrib/sessions/management/commands/clearsessions.py3
-rw-r--r--django/contrib/sessions/middleware.py2
-rw-r--r--django/contrib/sitemaps/__init__.py12
-rw-r--r--django/contrib/sitemaps/tests/test_http.py15
-rw-r--r--django/contrib/sitemaps/tests/urls/http.py28
-rw-r--r--django/contrib/sitemaps/views.py12
-rw-r--r--django/contrib/staticfiles/finders.py9
-rw-r--r--django/contrib/staticfiles/management/commands/collectstatic.py4
-rw-r--r--django/contrib/staticfiles/storage.py8
-rw-r--r--django/contrib/staticfiles/views.py5
-rw-r--r--django/core/cache/__init__.py2
-rw-r--r--django/core/cache/utils.py2
-rw-r--r--django/core/handlers/wsgi.py2
-rw-r--r--django/core/mail/__init__.py10
-rw-r--r--django/core/management/__init__.py8
-rw-r--r--django/core/management/base.py10
-rw-r--r--django/core/management/color.py2
-rw-r--r--django/core/management/commands/dumpdata.py11
-rw-r--r--django/core/management/commands/flush.py1
-rw-r--r--django/core/management/commands/inspectdb.py6
-rw-r--r--django/core/management/commands/loaddata.py14
-rw-r--r--django/core/management/commands/migrate.py4
-rw-r--r--django/core/management/commands/runfcgi.py8
-rw-r--r--django/core/management/commands/shell.py40
-rw-r--r--django/core/management/commands/startapp.py3
-rw-r--r--django/core/management/commands/startproject.py3
-rw-r--r--django/core/management/sql.py20
-rw-r--r--django/core/management/utils.py2
-rw-r--r--django/core/serializers/__init__.py3
-rw-r--r--django/core/serializers/json.py1
-rw-r--r--django/core/servers/fastcgi.py2
-rw-r--r--django/core/urlresolvers.py2
-rw-r--r--django/db/backends/__init__.py2
-rw-r--r--django/db/backends/creation.py2
-rw-r--r--django/db/backends/oracle/base.py2
-rw-r--r--django/db/backends/oracle/compiler.py2
-rw-r--r--django/db/backends/sqlite3/base.py2
-rw-r--r--django/db/models/__init__.py4
-rw-r--r--django/db/models/deletion.py4
-rw-r--r--django/db/models/fields/__init__.py27
-rw-r--r--django/db/models/fields/files.py1
-rw-r--r--django/db/models/fields/related.py20
-rw-r--r--django/db/models/loading.py51
-rw-r--r--django/db/models/manager.py174
-rw-r--r--django/db/models/options.py23
-rw-r--r--django/db/models/query.py119
-rw-r--r--django/db/models/sql/__init__.py2
-rw-r--r--django/db/models/sql/query.py68
-rw-r--r--django/db/models/sql/subqueries.py12
-rw-r--r--django/db/models/sql/where.py2
-rw-r--r--django/db/utils.py4
-rw-r--r--django/forms/__init__.py2
-rw-r--r--django/forms/extras/__init__.py2
-rw-r--r--django/forms/fields.py37
-rw-r--r--django/forms/forms.py20
-rw-r--r--django/forms/formsets.py2
-rw-r--r--django/forms/models.py13
-rw-r--r--django/forms/widgets.py9
-rw-r--r--django/http/cookie.py2
-rw-r--r--django/http/request.py11
-rw-r--r--django/http/response.py6
-rw-r--r--django/middleware/locale.py5
-rw-r--r--django/template/base.py4
-rw-r--r--django/template/loaders/app_directories.py2
-rw-r--r--django/template/loaders/cached.py39
-rw-r--r--django/test/client.py2
-rw-r--r--django/test/simple.py2
-rw-r--r--django/utils/datastructures.py6
-rw-r--r--django/utils/formats.py4
-rw-r--r--django/utils/html.py15
-rw-r--r--django/utils/importlib.py5
-rw-r--r--django/utils/module_loading.py2
-rw-r--r--django/utils/translation/trans_real.py10
-rw-r--r--django/views/decorators/debug.py6
-rw-r--r--django/views/defaults.py11
-rw-r--r--django/views/generic/detail.py20
-rw-r--r--django/views/generic/list.py20
-rw-r--r--django/views/i18n.py2
167 files changed, 1256 insertions, 730 deletions
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
--- a/django/contrib/admin/static/admin/img/chooser-bg.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/chooser_stacked-bg.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tool-left.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tool-left_over.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tool-right.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tool-right_over.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tooltag-add.gif
+++ /dev/null
Binary files 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
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/tooltag-add.png
Binary files 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
--- a/django/contrib/admin/static/admin/img/tooltag-add_over.gif
+++ /dev/null
Binary files 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
--- a/django/contrib/admin/static/admin/img/tooltag-arrowright.gif
+++ /dev/null
Binary files 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
--- /dev/null
+++ b/django/contrib/admin/static/admin/img/tooltag-arrowright.png
Binary files 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
--- a/django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif
+++ /dev/null
Binary files 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(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset",
-[a(this).attr("id")]);return!1})})})(django.jQuery);
+(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){a(b).find("div.errors").length==0&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset",
+[a(this).attr("id")]);return false})})})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js
index b89aedd4cc..cc888a5628 100644
--- a/django/contrib/admin/static/admin/js/inlines.min.js
+++ b/django/contrib/admin/static/admin/js/inlines.min.js
@@ -1,9 +1,9 @@
-(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),f=""===e.val()||0<e.val()-f.val();c.each(function(){b(this).not("."+
-a.emptyCssClass).addClass(a.formCssClass)});if(c.length&&f){var h;"TR"==c.prop("tagName")?(c=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+
-"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){i(this,
-a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c<f;c++){i(b(d).get(c),a.prefix,c);b(d.get(c)).find("*").each(function(){i(this,
-a.prefix,c)})}});a.added&&a.added(c)})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),c=function(){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+
-d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=
-typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),c=function(){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix,
-addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".form-row .field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),
-DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a}})(django.jQuery);
+(function(a){a.fn.formset=function(g){var b=a.extend({},a.fn.formset.defaults,g),i=a(this);g=i.parent();var m=function(e,k,h){var j=RegExp("("+k+"-(\\d+|__prefix__))");k=k+"-"+h;a(e).prop("for")&&a(e).prop("for",a(e).prop("for").replace(j,k));if(e.id)e.id=e.id.replace(j,k);if(e.name)e.name=e.name.replace(j,k)},l=a("#id_"+b.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),d=parseInt(l.val(),10),c=a("#id_"+b.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off");l=c.val()===""||c.val()-l.val()>0;i.each(function(){a(this).not("."+
+b.emptyCssClass).addClass(b.formCssClass)});if(i.length&&l){var f;if(i.prop("tagName")=="TR"){i=this.eq(-1).children().length;g.append('<tr class="'+b.addCssClass+'"><td colspan="'+i+'"><a href="javascript:void(0)">'+b.addText+"</a></tr>");f=g.find("tr:last a")}else{i.filter(":last").after('<div class="'+b.addCssClass+'"><a href="javascript:void(0)">'+b.addText+"</a></div>");f=i.filter(":last").next().find("a")}f.click(function(e){e.preventDefault();var k=a("#id_"+b.prefix+"-TOTAL_FORMS");e=a("#"+
+b.prefix+"-empty");var h=e.clone(true);h.removeClass(b.emptyCssClass).addClass(b.formCssClass).attr("id",b.prefix+"-"+d);if(h.is("tr"))h.children(":last").append('<div><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></div>");else h.is("ul")||h.is("ol")?h.append('<li><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></li>"):h.children(":first").append('<span><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></span>");
+h.find("*").each(function(){m(this,b.prefix,k.val())});h.insertBefore(a(e));a(k).val(parseInt(k.val(),10)+1);d+=1;c.val()!==""&&c.val()-k.val()<=0&&f.parent().hide();h.find("a."+b.deleteCssClass).click(function(j){j.preventDefault();j=a(this).parents("."+b.formCssClass);j.remove();d-=1;b.removed&&b.removed(j);j=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(j.length);if(c.val()===""||c.val()-j.length>0)f.parent().show();for(var n=0,o=j.length;n<o;n++){m(a(j).get(n),b.prefix,n);a(j.get(n)).find("*").each(function(){m(this,
+b.prefix,n)})}});b.added&&b.added(h)})}return this};a.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};a.fn.tabularFormset=function(g){var b=a(this),i=function(){a(b.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},m=function(){if(typeof SelectFilter!="undefined"){a(".selectfilter").each(function(d,
+c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".field-"+h).find("input, select, textarea").attr("id"))});e.length&&c.prepopulate(e,c.attr("maxlength"))})};
+b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b};a.fn.stackedFormset=function(g){var b=a(this),i=function(){a(b.selector).find(".inline_label").each(function(d){d=d+1;a(this).html(a(this).html().replace(/(#\d+)/g,"#"+d))})},m=function(){if(typeof SelectFilter!=
+"undefined"){a(".selectfilter").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".form-row .field-"+h).find("input, select, textarea").attr("id"))});
+e.length&&c.prepopulate(e,c.attr("maxlength"))})};b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b}})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/prepopulate.js b/django/contrib/admin/static/admin/js/prepopulate.js
index 24f24f95a8..db7903a5ef 100644
--- a/django/contrib/admin/static/admin/js/prepopulate.js
+++ b/django/contrib/admin/static/admin/js/prepopulate.js
@@ -3,32 +3,37 @@
/*
Depends on urlify.js
Populates a selected field with the values of the dependent fields,
- URLifies and shortens the string.
- dependencies - array of dependent fields id's
- maxLength - maximum length of the URLify'd string
+ URLifies and shortens the string.
+ dependencies - array of dependent fields ids
+ maxLength - maximum length of the URLify'd string
*/
return this.each(function() {
- var field = $(this);
-
- field.data('_changed', false);
- field.change(function() {
- field.data('_changed', true);
- });
+ var prepopulatedField = $(this);
var populate = function () {
- // Bail if the fields value has changed
- if (field.data('_changed') == true) return;
-
+ // Bail if the field's value has been changed by the user
+ if (prepopulatedField.data('_changed')) {
+ return;
+ }
+
var values = [];
$.each(dependencies, function(i, field) {
- if ($(field).val().length > 0) {
- values.push($(field).val());
- }
- })
- field.val(URLify(values.join(' '), maxLength));
+ field = $(field);
+ if (field.val().length > 0) {
+ values.push(field.val());
+ }
+ });
+ prepopulatedField.val(URLify(values.join(' '), maxLength));
};
- $(dependencies.join(',')).keyup(populate).change(populate).focus(populate);
+ prepopulatedField.data('_changed', false);
+ prepopulatedField.change(function() {
+ prepopulatedField.data('_changed', true);
+ });
+
+ if (!prepopulatedField.val()) {
+ $(dependencies.join(',')).keyup(populate).change(populate).focus(populate);
+ }
});
};
})(django.jQuery);
diff --git a/django/contrib/admin/static/admin/js/prepopulate.min.js b/django/contrib/admin/static/admin/js/prepopulate.min.js
index aa94937963..4a75827c97 100644
--- a/django/contrib/admin/static/admin/js/prepopulate.min.js
+++ b/django/contrib/admin/static/admin/js/prepopulate.min.js
@@ -1 +1 @@
-(function(a){a.fn.prepopulate=function(d,g){return this.each(function(){var b=a(this);b.data("_changed",false);b.change(function(){b.data("_changed",true)});var c=function(){if(b.data("_changed")!=true){var e=[];a.each(d,function(h,f){a(f).val().length>0&&e.push(a(f).val())});b.val(URLify(e.join(" "),g))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery);
+(function(b){b.fn.prepopulate=function(e,g){return this.each(function(){var a=b(this),d=function(){if(!a.data("_changed")){var f=[];b.each(e,function(h,c){c=b(c);c.val().length>0&&f.push(c.val())});a.val(URLify(f.join(" "),g))}};a.data("_changed",false);a.change(function(){a.data("_changed",true)});a.val()||b(e.join(",")).keyup(d).change(d).focus(d)})}})(django.jQuery);
diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html
index 1c928a0d4d..3e557ebef3 100644
--- a/django/contrib/admin/templates/registration/password_change_done.html
+++ b/django/contrib/admin/templates/registration/password_change_done.html
@@ -8,12 +8,8 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password change successful' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-
-<h1>{% trans 'Password change successful' %}</h1>
-
<p>{% trans 'Your password was changed.' %}</p>
-
{% endblock %}
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index f7316a739f..921df0ac72 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -9,7 +9,8 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password change' %}{% endblock %}
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}<div id="content-main">
@@ -21,7 +22,6 @@
</p>
{% endif %}
-<h1>{% trans 'Password change' %}</h1>
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
diff --git a/django/contrib/admin/templates/registration/password_reset_complete.html b/django/contrib/admin/templates/registration/password_reset_complete.html
index d97f338197..19f87a5b76 100644
--- a/django/contrib/admin/templates/registration/password_reset_complete.html
+++ b/django/contrib/admin/templates/registration/password_reset_complete.html
@@ -8,12 +8,11 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset complete' %}{% endblock %}
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans 'Password reset complete' %}</h1>
-
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p>
diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html
index 81020b929f..bd24806d46 100644
--- a/django/contrib/admin/templates/registration/password_reset_confirm.html
+++ b/django/contrib/admin/templates/registration/password_reset_confirm.html
@@ -8,14 +8,12 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
{% if validlink %}
-<h1>{% trans 'Enter new password' %}</h1>
-
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form action="" method="post">{% csrf_token %}
@@ -28,8 +26,6 @@
{% else %}
-<h1>{% trans 'Password reset unsuccessful' %}</h1>
-
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html
index 98471041b5..d157306a91 100644
--- a/django/contrib/admin/templates/registration/password_reset_done.html
+++ b/django/contrib/admin/templates/registration/password_reset_done.html
@@ -8,12 +8,10 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset successful' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans 'Password reset successful' %}</h1>
-
<p>{% trans "We've emailed you instructions for setting your password. You should be receiving them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html
index c9998a1a3b..dc05cd0245 100644
--- a/django/contrib/admin/templates/registration/password_reset_form.html
+++ b/django/contrib/admin/templates/registration/password_reset_form.html
@@ -8,12 +8,10 @@
</div>
{% endblock %}
-{% block title %}{% trans "Password reset" %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans "Password reset" %}</h1>
-
<p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p>
<form action="" method="post">{% csrf_token %}
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 8596dfb825..6c3c3e8511 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -180,7 +180,7 @@ def items_for_result(cl, result, form):
first = True
pk = cl.lookup_opts.pk.attname
for field_name in cl.list_display:
- row_class = ''
+ row_classes = ['field-%s' % field_name]
try:
f, attr, value = lookup_field(field_name, result, cl.model_admin)
except ObjectDoesNotExist:
@@ -188,7 +188,7 @@ def items_for_result(cl, result, form):
else:
if f is None:
if field_name == 'action_checkbox':
- row_class = mark_safe(' class="action-checkbox"')
+ row_classes = ['action-checkbox']
allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False)
if boolean:
@@ -199,7 +199,7 @@ def items_for_result(cl, result, form):
if allow_tags:
result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)):
- row_class = mark_safe(' class="nowrap"')
+ row_classes.append('nowrap')
else:
if isinstance(f.rel, models.ManyToOneRel):
field_val = getattr(result, f.name)
@@ -210,9 +210,10 @@ def items_for_result(cl, result, form):
else:
result_repr = display_for_field(value, f)
if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)):
- row_class = mark_safe(' class="nowrap"')
+ row_classes.append('nowrap')
if force_text(result_repr) == '':
result_repr = mark_safe('&nbsp;')
+ row_class = mark_safe(' class="%s"' % ' '.join(row_classes))
# If list_display_links not defined, add the link tag to the first field
if (first and not cl.list_display_links) or field_name in cl.list_display_links:
table_tag = {True:'th', False:'td'}[first]
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index 98ac1a657e..005bde3023 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -9,7 +9,7 @@ def prepopulated_fields_js(context):
the prepopulated fields for both the admin form and inlines.
"""
prepopulated_fields = []
- if context['add'] and 'adminform' in context:
+ if 'adminform' in context:
prepopulated_fields.extend(context['adminform'].prepopulated_fields)
if 'inline_admin_formsets' in context:
for inline_admin_formset in context['inline_admin_formsets']:
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index f766031bf8..3325747a9f 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
import sys
import warnings
@@ -7,7 +8,6 @@ from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.fields import FieldDoesNotExist
from django.utils import six
-from django.utils.datastructures import SortedDict
from django.utils.deprecation import RenameMethodsBase
from django.utils.encoding import force_str, force_text
from django.utils.translation import ugettext, ugettext_lazy
@@ -319,13 +319,13 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
def get_ordering_field_columns(self):
"""
- Returns a SortedDict of ordering field column numbers and asc/desc
+ Returns an OrderedDict of ordering field column numbers and asc/desc
"""
# We must cope with more than one column having the same underlying sort
# field, so we base things on column numbers.
ordering = self._get_default_ordering()
- ordering_fields = SortedDict()
+ ordering_fields = OrderedDict()
if ORDER_VAR not in self.params:
# for ordering specified on ModelAdmin or model Meta, we don't know
# the right column numbers absolutely, because there might be more
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 4b79401dbc..c4b15cdd6a 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -116,6 +116,8 @@ def url_params_from_lookup_dict(lookups):
if lookups and hasattr(lookups, 'items'):
items = []
for k, v in lookups.items():
+ if callable(v):
+ v = v()
if isinstance(v, (tuple, list)):
v = ','.join([str(x) for x in v])
elif isinstance(v, bool):
@@ -285,7 +287,14 @@ class AdminTextInputWidget(forms.TextInput):
final_attrs.update(attrs)
super(AdminTextInputWidget, self).__init__(attrs=final_attrs)
-class AdminURLFieldWidget(forms.TextInput):
+class AdminEmailInputWidget(forms.EmailInput):
+ def __init__(self, attrs=None):
+ final_attrs = {'class': 'vTextField'}
+ if attrs is not None:
+ final_attrs.update(attrs)
+ super(AdminEmailInputWidget, self).__init__(attrs=final_attrs)
+
+class AdminURLFieldWidget(forms.URLInput):
def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'}
if attrs is not None:
diff --git a/django/contrib/admindocs/tests/test_fields.py b/django/contrib/admindocs/tests/test_fields.py
index b505d2deeb..e8fe0b0caa 100644
--- a/django/contrib/admindocs/tests/test_fields.py
+++ b/django/contrib/admindocs/tests/test_fields.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import unittest
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index b3faf06e25..5515a3eee4 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -1,3 +1,4 @@
+from importlib import import_module
import inspect
import os
import re
@@ -13,7 +14,6 @@ from django.http import Http404
from django.core import urlresolvers
from django.contrib.admindocs import utils
from django.contrib.sites.models import Site
-from django.utils.importlib import import_module
from django.utils._os import upath
from django.utils import six
from django.utils.translation import ugettext as _
@@ -319,7 +319,7 @@ def load_all_installed_template_libraries():
libraries = []
for library_name in libraries:
try:
- lib = template.get_library(library_name)
+ template.get_library(library_name)
except template.InvalidTemplateLibrary:
pass
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index ca660606e5..ff08f41798 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect)
+sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
class GroupAdmin(admin.ModelAdmin):
@@ -87,7 +88,7 @@ class UserAdmin(admin.ModelAdmin):
return False
return super(UserAdmin, self).lookup_allowed(lookup, value)
- @sensitive_post_parameters()
+ @sensitive_post_parameters_m
@csrf_protect_m
@transaction.atomic
def add_view(self, request, form_url='', extra_context=None):
@@ -118,7 +119,7 @@ class UserAdmin(admin.ModelAdmin):
return super(UserAdmin, self).add_view(request, form_url,
extra_context)
- @sensitive_post_parameters()
+ @sensitive_post_parameters_m
def user_change_password(self, request, id, form_url=''):
if not self.has_change_permission(request):
raise PermissionDenied
@@ -127,6 +128,8 @@ class UserAdmin(admin.ModelAdmin):
form = self.change_password_form(user, request.POST)
if form.is_valid():
form.save()
+ change_message = self.construct_change_message(request, form, None)
+ self.log_change(request, request.user, change_message)
msg = ugettext('Password changed successfully.')
messages.success(request, msg)
return HttpResponseRedirect('..')
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
index 6b31f72b03..cb79291c17 100644
--- a/django/contrib/auth/backends.py
+++ b/django/contrib/auth/backends.py
@@ -17,7 +17,9 @@ class ModelBackend(object):
if user.check_password(password):
return user
except UserModel.DoesNotExist:
- return None
+ # Run the default password hasher once to reduce the timing
+ # difference between an existing and a non-existing user (#20760).
+ UserModel().set_password(password)
def get_group_permissions(self, user_obj, obj=None):
"""
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 2d7a7c14d4..3eba8abd51 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,9 +1,10 @@
from __future__ import unicode_literals
+from collections import OrderedDict
+
from django import forms
from django.forms.util import flatatt
from django.template import loader
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes
from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode
@@ -191,13 +192,28 @@ class AuthenticationForm(forms.Form):
code='invalid_login',
params={'username': self.username_field.verbose_name},
)
- elif not self.user_cache.is_active:
- raise forms.ValidationError(
- self.error_messages['inactive'],
- code='inactive',
- )
+ else:
+ self.confirm_login_allowed(self.user_cache)
+
return self.cleaned_data
+ def confirm_login_allowed(self, user):
+ """
+ Controls whether the given User may log in. This is a policy setting,
+ independent of end-user authentication. This default behavior is to
+ allow login by active users, and reject login by inactive users.
+
+ If the given user cannot log in, this method should raise a
+ ``forms.ValidationError``.
+
+ If the given user may log in, this method should return None.
+ """
+ if not user.is_active:
+ raise forms.ValidationError(
+ self.error_messages['inactive'],
+ code='inactive',
+ )
+
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
@@ -214,7 +230,7 @@ class PasswordResetForm(forms.Form):
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
- from_email=None, request=None):
+ from_email=None, request=None, html_email_template_name=None):
"""
Generates a one-use only link for resetting password and sends to the
user.
@@ -247,7 +263,12 @@ class PasswordResetForm(forms.Form):
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
email = loader.render_to_string(email_template_name, c)
- send_mail(subject, email, from_email, [user.email])
+
+ if html_email_template_name:
+ html_email = loader.render_to_string(html_email_template_name, c)
+ else:
+ html_email = None
+ send_mail(subject, email, from_email, [user.email], html_message=html_email)
class SetPasswordForm(forms.Form):
@@ -309,7 +330,7 @@ class PasswordChangeForm(SetPasswordForm):
)
return old_password
-PasswordChangeForm.base_fields = SortedDict([
+PasswordChangeForm.base_fields = OrderedDict([
(k, PasswordChangeForm.base_fields[k])
for k in ['old_password', 'new_password1', 'new_password2']
])
@@ -350,3 +371,11 @@ class AdminPasswordChangeForm(forms.Form):
if commit:
self.user.save()
return self.user
+
+ def _get_changed_data(self):
+ data = super(AdminPasswordChangeForm, self).changed_data
+ for name in self.fields.keys():
+ if name not in data:
+ return []
+ return ['password']
+ changed_data = property(_get_changed_data)
diff --git a/django/contrib/auth/hashers.py b/django/contrib/auth/hashers.py
index 7656b50437..c4dca38252 100644
--- a/django/contrib/auth/hashers.py
+++ b/django/contrib/auth/hashers.py
@@ -2,13 +2,13 @@ from __future__ import unicode_literals
import base64
import binascii
+from collections import OrderedDict
import hashlib
+import importlib
from django.dispatch import receiver
from django.conf import settings
from django.test.signals import setting_changed
-from django.utils import importlib
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes, force_str, force_text
from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import (
@@ -172,7 +172,7 @@ class BasePasswordHasher(object):
if isinstance(self.library, (tuple, list)):
name, mod_path = self.library
else:
- name = mod_path = self.library
+ mod_path = self.library
try:
module = importlib.import_module(mod_path)
except ImportError as e:
@@ -243,7 +243,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('iterations'), iterations),
(_('salt'), mask_hash(salt)),
@@ -320,7 +320,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
assert algorithm == self.algorithm
salt, checksum = data[:22], data[22:]
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('work factor'), work_factor),
(_('salt'), mask_hash(salt)),
@@ -368,7 +368,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
@@ -396,7 +396,7 @@ class MD5PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
@@ -429,7 +429,7 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded):
assert encoded.startswith('sha1$$')
hash = encoded[6:]
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(hash)),
])
@@ -462,7 +462,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(encoded, show=3)),
])
@@ -496,7 +496,7 @@ class CryptPasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded):
algorithm, salt, data = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), salt),
(_('hash'), mask_hash(data, show=3)),
diff --git a/django/contrib/auth/tests/templates/registration/html_password_reset_email.html b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html
new file mode 100644
index 0000000000..1ebb550048
--- /dev/null
+++ b/django/contrib/auth/tests/templates/registration/html_password_reset_email.html
@@ -0,0 +1 @@
+<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>
diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py
index fc5a80e8dd..4e83d786cf 100644
--- a/django/contrib/auth/tests/test_auth_backends.py
+++ b/django/contrib/auth/tests/test_auth_backends.py
@@ -12,6 +12,17 @@ from django.contrib.auth import authenticate, get_user
from django.http import HttpRequest
from django.test import TestCase
from django.test.utils import override_settings
+from django.contrib.auth.hashers import MD5PasswordHasher
+
+
+class CountingMD5PasswordHasher(MD5PasswordHasher):
+ """Hasher that counts how many times it computes a hash."""
+
+ calls = 0
+
+ def encode(self, *args, **kwargs):
+ type(self).calls += 1
+ return super(CountingMD5PasswordHasher, self).encode(*args, **kwargs)
class BaseModelBackendTest(object):
@@ -107,10 +118,26 @@ class BaseModelBackendTest(object):
self.assertEqual(user.get_all_permissions(), set(['auth.test']))
def test_get_all_superuser_permissions(self):
- "A superuser has all permissions. Refs #14795"
+ """A superuser has all permissions. Refs #14795."""
user = self.UserModel._default_manager.get(pk=self.superuser.pk)
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
+ @override_settings(PASSWORD_HASHERS=('django.contrib.auth.tests.test_auth_backends.CountingMD5PasswordHasher',))
+ def test_authentication_timing(self):
+ """Hasher is run once regardless of whether the user exists. Refs #20760."""
+ # Re-set the password, because this tests overrides PASSWORD_HASHERS
+ self.user.set_password('test')
+ self.user.save()
+
+ CountingMD5PasswordHasher.calls = 0
+ username = getattr(self.user, self.UserModel.USERNAME_FIELD)
+ authenticate(username=username, password='test')
+ self.assertEqual(CountingMD5PasswordHasher.calls, 1)
+
+ CountingMD5PasswordHasher.calls = 0
+ authenticate(username='no_such_user', password='test')
+ self.assertEqual(CountingMD5PasswordHasher.calls, 1)
+
@skipIfCustomUser
class ModelBackendTest(BaseModelBackendTest, TestCase):
diff --git a/django/contrib/auth/tests/test_context_processors.py b/django/contrib/auth/tests/test_context_processors.py
index 9e56cfce85..d1d912eb15 100644
--- a/django/contrib/auth/tests/test_context_processors.py
+++ b/django/contrib/auth/tests/test_context_processors.py
@@ -161,7 +161,7 @@ class AuthContextProcessorTests(TestCase):
# Exception RuntimeError: 'maximum recursion depth exceeded while
# calling a Python object' in <type 'exceptions.AttributeError'>
# ignored"
- query = Q(user=response.context['user']) & Q(someflag=True)
+ Q(user=response.context['user']) & Q(someflag=True)
# Tests for user equality. This is hard because User defines
# equality in a non-duck-typing way
diff --git a/django/contrib/auth/tests/test_forms.py b/django/contrib/auth/tests/test_forms.py
index 0b998105af..eef366f184 100644
--- a/django/contrib/auth/tests/test_forms.py
+++ b/django/contrib/auth/tests/test_forms.py
@@ -1,7 +1,9 @@
from __future__ import unicode_literals
import os
+import re
+from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
@@ -131,6 +133,40 @@ class AuthenticationFormTest(TestCase):
self.assertEqual(form.non_field_errors(),
[force_text(form.error_messages['inactive'])])
+ def test_custom_login_allowed_policy(self):
+ # The user is inactive, but our custom form policy allows him to log in.
+ data = {
+ 'username': 'inactive',
+ 'password': 'password',
+ }
+
+ class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm):
+ def confirm_login_allowed(self, user):
+ pass
+
+ form = AuthenticationFormWithInactiveUsersOkay(None, data)
+ self.assertTrue(form.is_valid())
+
+ # If we want to disallow some logins according to custom logic,
+ # we should raise a django.forms.ValidationError in the form.
+ class PickyAuthenticationForm(AuthenticationForm):
+ def confirm_login_allowed(self, user):
+ if user.username == "inactive":
+ raise forms.ValidationError(_("This user is disallowed."))
+ raise forms.ValidationError(_("Sorry, nobody's allowed in."))
+
+ form = PickyAuthenticationForm(None, data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.non_field_errors(), ['This user is disallowed.'])
+
+ data = {
+ 'username': 'testclient',
+ 'password': 'password',
+ }
+ form = PickyAuthenticationForm(None, data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."])
+
def test_success(self):
# The success case
data = {
@@ -272,7 +308,7 @@ class UserChangeFormTest(TestCase):
fields = ('groups',)
# Just check we can create it
- form = MyUserForm({})
+ MyUserForm({})
def test_unsuable_password(self):
user = User.objects.get(username='empty_password')
@@ -417,6 +453,60 @@ class PasswordResetFormTest(TestCase):
form.save()
self.assertEqual(len(mail.outbox), 0)
+ @override_settings(
+ TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
+ TEMPLATE_DIRS=(
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
+ ),
+ )
+ def test_save_plaintext_email(self):
+ """
+ Test the PasswordResetForm.save() method with no html_email_template_name
+ parameter passed in.
+ Test to ensure original behavior is unchanged after the parameter was added.
+ """
+ (user, username, email) = self.create_dummy_user()
+ form = PasswordResetForm({"email": email})
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0].message()
+ self.assertFalse(message.is_multipart())
+ self.assertEqual(message.get_content_type(), 'text/plain')
+ self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
+ self.assertEqual(len(mail.outbox[0].alternatives), 0)
+ self.assertEqual(message.get_all('to'), [email])
+ self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload()))
+
+ @override_settings(
+ TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
+ TEMPLATE_DIRS=(
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
+ ),
+ )
+ def test_save_html_email_template_name(self):
+ """
+ Test the PasswordResetFOrm.save() method with html_email_template_name
+ parameter specified.
+ Test to ensure that a multipart email is sent with both text/plain
+ and text/html parts.
+ """
+ (user, username, email) = self.create_dummy_user()
+ form = PasswordResetForm({"email": email})
+ self.assertTrue(form.is_valid())
+ form.save(html_email_template_name='registration/html_password_reset_email.html')
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(len(mail.outbox[0].alternatives), 1)
+ message = mail.outbox[0].message()
+ self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
+ self.assertEqual(len(message.get_payload()), 2)
+ self.assertTrue(message.is_multipart())
+ self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
+ self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
+ self.assertEqual(message.get_all('to'), [email])
+ self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload()))
+ self.assertTrue(re.match(r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$', message.get_payload(1).get_payload()))
+
class ReadOnlyPasswordHashTest(TestCase):
diff --git a/django/contrib/auth/tests/test_templates.py b/django/contrib/auth/tests/test_templates.py
new file mode 100644
index 0000000000..f7d74748a2
--- /dev/null
+++ b/django/contrib/auth/tests/test_templates.py
@@ -0,0 +1,59 @@
+from django.contrib.auth import authenticate
+from django.contrib.auth.models import User
+from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.contrib.auth.views import (
+ password_reset, password_reset_done, password_reset_confirm,
+ password_reset_complete, password_change, password_change_done,
+)
+from django.test import RequestFactory, TestCase
+from django.test.utils import override_settings
+from django.utils.encoding import force_bytes, force_text
+from django.utils.http import urlsafe_base64_encode
+
+
+@skipIfCustomUser
+@override_settings(
+ PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
+)
+class AuthTemplateTests(TestCase):
+
+ def test_titles(self):
+ rf = RequestFactory()
+ user = User.objects.create_user('jsmith', 'jsmith@example.com', 'pass')
+ user = authenticate(username=user.username, password='pass')
+ request = rf.get('/somepath/')
+ request.user = user
+
+ response = password_reset(request, post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Password reset</title>')
+ self.assertContains(response, '<h1>Password reset</h1>')
+
+ response = password_reset_done(request)
+ self.assertContains(response, '<title>Password reset successful</title>')
+ self.assertContains(response, '<h1>Password reset successful</h1>')
+
+ # password_reset_confirm invalid token
+ response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Password reset unsuccessful</title>')
+ self.assertContains(response, '<h1>Password reset unsuccessful</h1>')
+
+ # password_reset_confirm valid token
+ default_token_generator = PasswordResetTokenGenerator()
+ token = default_token_generator.make_token(user)
+ uidb64 = force_text(urlsafe_base64_encode(force_bytes(user.pk)))
+ response = password_reset_confirm(request, uidb64, token, post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Enter new password</title>')
+ self.assertContains(response, '<h1>Enter new password</h1>')
+
+ response = password_reset_complete(request)
+ self.assertContains(response, '<title>Password reset complete</title>')
+ self.assertContains(response, '<h1>Password reset complete</h1>')
+
+ response = password_change(request, post_change_redirect='dummy/')
+ self.assertContains(response, '<title>Password change</title>')
+ self.assertContains(response, '<h1>Password change</h1>')
+
+ response = password_change_done(request)
+ self.assertContains(response, '<title>Password change successful</title>')
+ self.assertContains(response, '<h1>Password change successful</h1>')
diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py
index b939dff058..22ccbfd225 100644
--- a/django/contrib/auth/tests/test_views.py
+++ b/django/contrib/auth/tests/test_views.py
@@ -8,6 +8,7 @@ except ImportError: # Python 2
from django.conf import global_settings, settings
from django.contrib.sites.models import Site, RequestSite
+from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from django.core import mail
from django.core.urlresolvers import reverse, NoReverseMatch
@@ -54,6 +55,11 @@ class AuthViewsTestCase(TestCase):
self.assertTrue(SESSION_KEY in self.client.session)
return response
+ def logout(self):
+ response = self.client.get('/admin/logout/')
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(SESSION_KEY not in self.client.session)
+
def assertFormError(self, response, error):
"""Assert that error is found in response.context['form'] errors"""
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
@@ -122,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertTrue("http://" in mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
+ # optional multipart text/html email has been added. Make sure original,
+ # default functionality is 100% the same
+ self.assertFalse(mail.outbox[0].message().is_multipart())
+
+ def test_html_mail_template(self):
+ """
+ A multipart email with text/plain and text/html is sent
+ if the html_email_template parameter is passed to the view
+ """
+ response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0].message()
+ self.assertEqual(len(message.get_payload()), 2)
+ self.assertTrue(message.is_multipart())
+ self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
+ self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
+ self.assertTrue('<html>' not in message.get_payload(0).get_payload())
+ self.assertTrue('<html>' in message.get_payload(1).get_payload())
def test_email_found_custom_from(self):
"Email is sent if a valid email address is provided for password reset when a custom from_email is provided."
@@ -178,7 +203,7 @@ class PasswordResetTest(AuthViewsTestCase):
def _test_confirm_start(self):
# Start by creating the email
- response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
+ self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
self.assertEqual(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0])
@@ -322,7 +347,7 @@ class ChangePasswordTest(AuthViewsTestCase):
})
def logout(self):
- response = self.client.get('/logout/')
+ self.client.get('/logout/')
def test_password_change_fails_with_invalid_old_password(self):
self.login()
@@ -344,7 +369,7 @@ class ChangePasswordTest(AuthViewsTestCase):
def test_password_change_succeeds(self):
self.login()
- response = self.client.post('/password_change/', {
+ self.client.post('/password_change/', {
'old_password': 'password',
'new_password1': 'password1',
'new_password2': 'password1',
@@ -459,7 +484,7 @@ class LoginTest(AuthViewsTestCase):
def test_login_form_contains_request(self):
# 15198
- response = self.client.post('/custom_requestauth_login/', {
+ self.client.post('/custom_requestauth_login/', {
'username': 'testclient',
'password': 'password',
}, follow=True)
@@ -670,18 +695,70 @@ class LogoutTest(AuthViewsTestCase):
self.confirm_logged_out()
@skipIfCustomUser
+@override_settings(
+ PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
+)
class ChangelistTests(AuthViewsTestCase):
urls = 'django.contrib.auth.tests.urls_admin'
- # #20078 - users shouldn't be allowed to guess password hashes via
- # repeated password__startswith queries.
- def test_changelist_disallows_password_lookups(self):
- # Make me a superuser before loging in.
+ def setUp(self):
+ # Make me a superuser before logging in.
User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True)
self.login()
+ self.admin = User.objects.get(pk=1)
+ def get_user_data(self, user):
+ return {
+ 'username': user.username,
+ 'password': user.password,
+ 'email': user.email,
+ 'is_active': user.is_active,
+ 'is_staff': user.is_staff,
+ 'is_superuser': user.is_superuser,
+ 'last_login_0': user.last_login.strftime('%Y-%m-%d'),
+ 'last_login_1': user.last_login.strftime('%H:%M:%S'),
+ 'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'),
+ 'initial-last_login_1': user.last_login.strftime('%H:%M:%S'),
+ 'date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
+ 'date_joined_1': user.date_joined.strftime('%H:%M:%S'),
+ 'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
+ 'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'),
+ 'first_name': user.first_name,
+ 'last_name': user.last_name,
+ }
+
+ # #20078 - users shouldn't be allowed to guess password hashes via
+ # repeated password__startswith queries.
+ def test_changelist_disallows_password_lookups(self):
# A lookup that tries to filter on password isn't OK
with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls:
response = self.client.get('/admin/auth/user/?password__startswith=sha1$')
self.assertEqual(response.status_code, 400)
self.assertEqual(len(logger_calls), 1)
+
+ def test_user_change_email(self):
+ data = self.get_user_data(self.admin)
+ data['email'] = 'new_' + data['email']
+ response = self.client.post('/admin/auth/user/%s/' % self.admin.pk, data)
+ self.assertRedirects(response, '/admin/auth/user/')
+ row = LogEntry.objects.latest('id')
+ self.assertEqual(row.change_message, 'Changed email.')
+
+ def test_user_not_change(self):
+ response = self.client.post('/admin/auth/user/%s/' % self.admin.pk,
+ self.get_user_data(self.admin)
+ )
+ self.assertRedirects(response, '/admin/auth/user/')
+ row = LogEntry.objects.latest('id')
+ self.assertEqual(row.change_message, 'No fields changed.')
+
+ def test_user_change_password(self):
+ response = self.client.post('/admin/auth/user/%s/password/' % self.admin.pk, {
+ 'password1': 'password1',
+ 'password2': 'password1',
+ })
+ self.assertRedirects(response, '/admin/auth/user/%s/' % self.admin.pk)
+ row = LogEntry.objects.latest('id')
+ self.assertEqual(row.change_message, 'Changed password.')
+ self.logout()
+ self.login(password='password1')
diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py
index 502fc659d4..2af83d21ea 100644
--- a/django/contrib/auth/tests/urls.py
+++ b/django/contrib/auth/tests/urls.py
@@ -67,6 +67,7 @@ urlpatterns = urlpatterns + patterns('',
(r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
(r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')),
(r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')),
+ (r'^password_reset/html_email_template/$', 'django.contrib.auth.views.password_reset', dict(html_email_template_name='registration/html_password_reset_email.html')),
(r'^reset/custom/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
'django.contrib.auth.views.password_reset_confirm',
dict(post_reset_redirect='/custom/')),
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index b731a2b3d1..d852106a4e 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -7,10 +7,9 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, QueryDict
from django.template.response import TemplateResponse
-from django.utils.http import base36_to_int, is_safe_url, urlsafe_base64_decode, urlsafe_base64_encode
+from django.utils.http import is_safe_url, urlsafe_base64_decode
from django.utils.translation import ugettext as _
from django.shortcuts import resolve_url
-from django.utils.encoding import force_bytes, force_text
from django.views.decorators.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
@@ -141,7 +140,8 @@ def password_reset(request, is_admin_site=False,
post_reset_redirect=None,
from_email=None,
current_app=None,
- extra_context=None):
+ extra_context=None,
+ html_email_template_name=None):
if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_done')
else:
@@ -156,6 +156,7 @@ def password_reset(request, is_admin_site=False,
'email_template_name': email_template_name,
'subject_template_name': subject_template_name,
'request': request,
+ 'html_email_template_name': html_email_template_name,
}
if is_admin_site:
opts = dict(opts, domain_override=request.get_host())
@@ -165,6 +166,7 @@ def password_reset(request, is_admin_site=False,
form = password_reset_form()
context = {
'form': form,
+ 'title': _('Password reset'),
}
if extra_context is not None:
context.update(extra_context)
@@ -175,7 +177,9 @@ def password_reset(request, is_admin_site=False,
def password_reset_done(request,
template_name='registration/password_reset_done.html',
current_app=None, extra_context=None):
- context = {}
+ context = {
+ 'title': _('Password reset successful'),
+ }
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context,
@@ -209,6 +213,7 @@ def password_reset_confirm(request, uidb64=None, token=None,
if user is not None and token_generator.check_token(user, token):
validlink = True
+ title = _('Enter new password')
if request.method == 'POST':
form = set_password_form(user, request.POST)
if form.is_valid():
@@ -219,8 +224,10 @@ def password_reset_confirm(request, uidb64=None, token=None,
else:
validlink = False
form = None
+ title = _('Password reset unsuccessful')
context = {
'form': form,
+ 'title': title,
'validlink': validlink,
}
if extra_context is not None:
@@ -232,7 +239,8 @@ def password_reset_complete(request,
template_name='registration/password_reset_complete.html',
current_app=None, extra_context=None):
context = {
- 'login_url': resolve_url(settings.LOGIN_URL)
+ 'login_url': resolve_url(settings.LOGIN_URL),
+ 'title': _('Password reset complete'),
}
if extra_context is not None:
context.update(extra_context)
@@ -261,6 +269,7 @@ def password_change(request,
form = password_change_form(user=request.user)
context = {
'form': form,
+ 'title': _('Password change'),
}
if extra_context is not None:
context.update(extra_context)
@@ -272,7 +281,9 @@ def password_change(request,
def password_change_done(request,
template_name='registration/password_change_done.html',
current_app=None, extra_context=None):
- context = {}
+ context = {
+ 'title': _('Password change successful'),
+ }
if extra_context is not None:
context.update(extra_context)
return TemplateResponse(request, template_name, context,
diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py
index 0b3fcebc51..56ed32bafe 100644
--- a/django/contrib/comments/__init__.py
+++ b/django/contrib/comments/__init__.py
@@ -1,10 +1,10 @@
+from importlib import import_module
import warnings
from django.conf import settings
from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured
from django.contrib.comments.models import Comment
from django.contrib.comments.forms import CommentForm
-from django.utils.importlib import import_module
warnings.warn("django.contrib.comments is deprecated and will be removed before Django 1.8.", DeprecationWarning)
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index befd326092..a2cbe33c0e 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
from django import http
from django.conf import settings
from django.contrib import comments
diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py
index 31bb98fa63..484ceac02c 100644
--- a/django/contrib/comments/views/moderation.py
+++ b/django/contrib/comments/views/moderation.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
from django import template
from django.conf import settings
from django.contrib import comments
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index 6f120f82e0..186f39068d 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -465,10 +465,10 @@ class GenericInlineModelAdmin(InlineModelAdmin):
formset = BaseGenericInlineFormSet
def get_formset(self, request, obj=None, **kwargs):
- if self.declared_fieldsets:
- fields = flatten_fieldsets(self.declared_fieldsets)
+ if 'fields' in kwargs:
+ fields = kwargs.pop('fields')
else:
- fields = None
+ fields = flatten_fieldsets(self.get_fieldsets(request, obj))
if self.exclude is None:
exclude = []
else:
diff --git a/django/contrib/flatpages/tests/test_csrf.py b/django/contrib/flatpages/tests/test_csrf.py
index cb51c124b8..d8f82b5d43 100644
--- a/django/contrib/flatpages/tests/test_csrf.py
+++ b/django/contrib/flatpages/tests/test_csrf.py
@@ -15,6 +15,7 @@ from django.test.utils import override_settings
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
),
+ CSRF_FAILURE_VIEW='django.views.csrf.csrf_failure',
TEMPLATE_DIRS=(
os.path.join(os.path.dirname(__file__), 'templates'),
),
diff --git a/django/contrib/formtools/preview.py b/django/contrib/formtools/preview.py
index d2e6987fa0..53b0da6e8e 100644
--- a/django/contrib/formtools/preview.py
+++ b/django/contrib/formtools/preview.py
@@ -39,7 +39,7 @@ class FormPreview(object):
"""
while 1:
try:
- f = self.form.base_fields[name]
+ self.form.base_fields[name]
except KeyError:
break # This field name isn't being used by the form.
name += '_'
diff --git a/django/contrib/formtools/tests/urls.py b/django/contrib/formtools/tests/urls.py
index f96f89ecdf..c86a7408e0 100644
--- a/django/contrib/formtools/tests/urls.py
+++ b/django/contrib/formtools/tests/urls.py
@@ -2,8 +2,6 @@
This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only.
"""
-from __future__ import absolute_import
-
from django.conf.urls import patterns, url
from django.contrib.formtools.tests.tests import TestFormPreview
diff --git a/django/contrib/formtools/tests/wizard/storage.py b/django/contrib/formtools/tests/wizard/storage.py
index 17968dfcda..c54ed9a058 100644
--- a/django/contrib/formtools/tests/wizard/storage.py
+++ b/django/contrib/formtools/tests/wizard/storage.py
@@ -1,8 +1,8 @@
from datetime import datetime
+from importlib import import_module
from django.http import HttpRequest
from django.conf import settings
-from django.utils.importlib import import_module
from django.contrib.auth.models import User
diff --git a/django/contrib/formtools/tests/wizard/test_forms.py b/django/contrib/formtools/tests/wizard/test_forms.py
index 21917822a7..ba3fbfdd48 100644
--- a/django/contrib/formtools/tests/wizard/test_forms.py
+++ b/django/contrib/formtools/tests/wizard/test_forms.py
@@ -1,11 +1,12 @@
from __future__ import unicode_literals
+from importlib import import_module
+
from django import forms, http
from django.conf import settings
from django.db import models
from django.test import TestCase
from django.template.response import TemplateResponse
-from django.utils.importlib import import_module
from django.contrib.auth.models import User
diff --git a/django/contrib/formtools/tests/wizard/wizardtests/forms.py b/django/contrib/formtools/tests/wizard/wizardtests/forms.py
index 6a8132971b..dbb0e83d6f 100644
--- a/django/contrib/formtools/tests/wizard/wizardtests/forms.py
+++ b/django/contrib/formtools/tests/wizard/wizardtests/forms.py
@@ -9,10 +9,9 @@ from django.forms.models import modelformset_factory
from django.http import HttpResponse
from django.template import Template, Context
-from django.contrib.auth.models import User
-
from django.contrib.formtools.wizard.views import WizardView
+
temp_storage_location = tempfile.mkdtemp(dir=os.environ.get('DJANGO_TEST_TEMP_DIR'))
temp_storage = FileSystemStorage(location=temp_storage_location)
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
index c478f20854..9c81f77ed2 100644
--- a/django/contrib/formtools/wizard/views.py
+++ b/django/contrib/formtools/wizard/views.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
import re
from django import forms
@@ -5,7 +6,6 @@ from django.shortcuts import redirect
from django.core.urlresolvers import reverse
from django.forms import formsets, ValidationError
from django.views.generic import TemplateView
-from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext as _
from django.utils import six
@@ -17,7 +17,7 @@ from django.contrib.formtools.wizard.forms import ManagementForm
def normalize_name(name):
"""
- Converts camel-case style names into underscore seperated words. Example::
+ Converts camel-case style names into underscore separated words. Example::
>>> normalize_name('oneTwoThree')
'one_two_three'
@@ -158,7 +158,7 @@ class WizardView(TemplateView):
form_list = form_list or kwargs.pop('form_list',
getattr(cls, 'form_list', None)) or []
- computed_form_list = SortedDict()
+ computed_form_list = OrderedDict()
assert len(form_list) > 0, 'at least one form is needed'
@@ -206,7 +206,7 @@ class WizardView(TemplateView):
The form_list is always generated on the fly because condition methods
could use data from other (maybe previous forms).
"""
- form_list = SortedDict()
+ form_list = OrderedDict()
for form_key, form_class in six.iteritems(self.form_list):
# try to fetch the value from condition list, by default, the form
# gets passed to the new list.
@@ -498,9 +498,10 @@ class WizardView(TemplateView):
if step is None:
step = self.steps.current
form_list = self.get_form_list()
- key = form_list.keyOrder.index(step) + 1
- if len(form_list.keyOrder) > key:
- return form_list.keyOrder[key]
+ keys = list(form_list.keys())
+ key = keys.index(step) + 1
+ if len(keys) > key:
+ return keys[key]
return None
def get_prev_step(self, step=None):
@@ -512,9 +513,10 @@ class WizardView(TemplateView):
if step is None:
step = self.steps.current
form_list = self.get_form_list()
- key = form_list.keyOrder.index(step) - 1
+ keys = list(form_list.keys())
+ key = keys.index(step) - 1
if key >= 0:
- return form_list.keyOrder[key]
+ return keys[key]
return None
def get_step_index(self, step=None):
@@ -524,7 +526,7 @@ class WizardView(TemplateView):
"""
if step is None:
step = self.steps.current
- return self.get_form_list().keyOrder.index(step)
+ return list(self.get_form_list().keys()).index(step)
def get_context_data(self, form, **kwargs):
"""
diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py
index 7e79be1860..3c748b5b80 100644
--- a/django/contrib/gis/admin/options.py
+++ b/django/contrib/gis/admin/options.py
@@ -55,7 +55,7 @@ class GeoModelAdmin(ModelAdmin):
3D editing).
"""
if isinstance(db_field, models.GeometryField) and db_field.dim < 3:
- request = kwargs.pop('request', None)
+ kwargs.pop('request', None)
# Setting the widget with the newly defined widget.
kwargs['widget'] = self.get_map_widget(db_field)
return db_field.formfield(**kwargs)
diff --git a/django/contrib/gis/db/backends/spatialite/creation.py b/django/contrib/gis/db/backends/spatialite/creation.py
index 2f0720ed84..22457dd4de 100644
--- a/django/contrib/gis/db/backends/spatialite/creation.py
+++ b/django/contrib/gis/db/backends/spatialite/creation.py
@@ -1,10 +1,12 @@
import os
+
from django.conf import settings
from django.core.cache import get_cache
from django.core.cache.backends.db import BaseDatabaseCache
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.sqlite3.creation import DatabaseCreation
+
class SpatiaLiteCreation(DatabaseCreation):
def create_test_db(self, verbosity=1, autoclobber=False):
@@ -53,8 +55,6 @@ class SpatiaLiteCreation(DatabaseCreation):
interactive=False,
database=self.connection.alias)
- from django.core.cache import get_cache
- from django.core.cache.backends.db import BaseDatabaseCache
for cache_alias in settings.CACHES:
cache = get_cache(cache_alias)
if isinstance(cache, BaseDatabaseCache):
@@ -62,7 +62,7 @@ class SpatiaLiteCreation(DatabaseCreation):
# Get a cursor (even though we don't need one yet). This has
# the side effect of initializing the test database.
- cursor = self.connection.cursor()
+ self.connection.cursor()
return test_database_name
diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py
index 2e221b7477..d29705986f 100644
--- a/django/contrib/gis/db/models/fields.py
+++ b/django/contrib/gis/db/models/fields.py
@@ -148,6 +148,7 @@ class GeometryField(Field):
value properly, and preserve any other lookup parameters before
returning to the caller.
"""
+ value = super(GeometryField, self).get_prep_value(value)
if isinstance(value, SQLEvaluator):
return value
elif isinstance(value, (tuple, list)):
diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py
index d75e8cd288..c5a87f40e0 100644
--- a/django/contrib/gis/gdal/geometries.py
+++ b/django/contrib/gis/gdal/geometries.py
@@ -101,7 +101,7 @@ class OGRGeometry(GDALBase):
else:
# Seeing if the input is a valid short-hand string
# (e.g., 'Point', 'POLYGON').
- ogr_t = OGRGeomType(geom_input)
+ OGRGeomType(geom_input)
g = capi.create_geom(OGRGeomType(geom_input).num)
elif isinstance(geom_input, memoryview):
# WKB was passed in
@@ -341,7 +341,7 @@ class OGRGeometry(GDALBase):
sz = self.wkb_size
# Creating the unsigned character buffer, and passing it in by reference.
buf = (c_ubyte * sz)()
- wkb = capi.to_wkb(self.ptr, byteorder, byref(buf))
+ capi.to_wkb(self.ptr, byteorder, byref(buf))
# Returning a buffer of the string at the pointer.
return memoryview(string_at(buf, sz))
diff --git a/django/contrib/gis/gdal/tests/test_envelope.py b/django/contrib/gis/gdal/tests/test_envelope.py
index 14258ff816..4644fbc533 100644
--- a/django/contrib/gis/gdal/tests/test_envelope.py
+++ b/django/contrib/gis/gdal/tests/test_envelope.py
@@ -22,9 +22,9 @@ class EnvelopeTest(unittest.TestCase):
def test01_init(self):
"Testing Envelope initilization."
e1 = Envelope((0, 0, 5, 5))
- e2 = Envelope(0, 0, 5, 5)
- e3 = Envelope(0, '0', '5', 5) # Thanks to ww for this
- e4 = Envelope(e1._envelope)
+ Envelope(0, 0, 5, 5)
+ Envelope(0, '0', '5', 5) # Thanks to ww for this
+ Envelope(e1._envelope)
self.assertRaises(OGRException, Envelope, (5, 5, 0, 0))
self.assertRaises(OGRException, Envelope, 5, 5, 0, 0)
self.assertRaises(OGRException, Envelope, (0, 0, 5, 5, 3))
diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py
index 74b1e894e1..41e7555ab3 100644
--- a/django/contrib/gis/gdal/tests/test_geom.py
+++ b/django/contrib/gis/gdal/tests/test_geom.py
@@ -25,15 +25,12 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
"Testing OGRGeomType object."
# OGRGeomType should initialize on all these inputs.
- try:
- g = OGRGeomType(1)
- g = OGRGeomType(7)
- g = OGRGeomType('point')
- g = OGRGeomType('GeometrycollectioN')
- g = OGRGeomType('LINearrING')
- g = OGRGeomType('Unknown')
- except:
- self.fail('Could not create an OGRGeomType object!')
+ OGRGeomType(1)
+ OGRGeomType(7)
+ OGRGeomType('point')
+ OGRGeomType('GeometrycollectioN')
+ OGRGeomType('LINearrING')
+ OGRGeomType('Unknown')
# Should throw TypeError on this input
self.assertRaises(OGRException, OGRGeomType, 23)
@@ -127,7 +124,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
def test02_points(self):
"Testing Point objects."
- prev = OGRGeometry('POINT(0 0)')
+ OGRGeometry('POINT(0 0)')
for p in self.geometries.points:
if not hasattr(p, 'z'): # No 3D
pnt = OGRGeometry(p.wkt)
@@ -243,7 +240,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
poly = OGRGeometry('POLYGON((0 0, 5 0, 5 5, 0 5), (1 1, 2 1, 2 2, 2 1))')
self.assertEqual(8, poly.point_count)
with self.assertRaises(OGRException):
- _ = poly.centroid
+ poly.centroid
poly.close_rings()
self.assertEqual(10, poly.point_count) # Two closing points should've been added
@@ -251,7 +248,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
def test08_multipolygons(self):
"Testing MultiPolygon objects."
- prev = OGRGeometry('POINT(0 0)')
+ OGRGeometry('POINT(0 0)')
for mp in self.geometries.multipolygons:
mpoly = OGRGeometry(mp.wkt)
self.assertEqual(6, mpoly.geom_type)
diff --git a/django/contrib/gis/gdal/tests/test_srs.py b/django/contrib/gis/gdal/tests/test_srs.py
index cacff4be04..33941458c8 100644
--- a/django/contrib/gis/gdal/tests/test_srs.py
+++ b/django/contrib/gis/gdal/tests/test_srs.py
@@ -58,7 +58,7 @@ class SpatialRefTest(unittest.TestCase):
def test01_wkt(self):
"Testing initialization on valid OGC WKT."
for s in srlist:
- srs = SpatialReference(s.wkt)
+ SpatialReference(s.wkt)
def test02_bad_wkt(self):
"Testing initialization on invalid WKT."
@@ -150,7 +150,7 @@ class SpatialRefTest(unittest.TestCase):
target = SpatialReference('WGS84')
for s in srlist:
if s.proj:
- ct = CoordTransform(SpatialReference(s.wkt), target)
+ CoordTransform(SpatialReference(s.wkt), target)
def test13_attr_value(self):
"Testing the attr_value() method."
diff --git a/django/contrib/gis/geoip/__init__.py b/django/contrib/gis/geoip/__init__.py
index 8b519a242d..c42dd2c72b 100644
--- a/django/contrib/gis/geoip/__init__.py
+++ b/django/contrib/gis/geoip/__init__.py
@@ -11,8 +11,6 @@
Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory
corresponding to settings.GEOIP_PATH.
"""
-from __future__ import absolute_import
-
try:
from .base import GeoIP, GeoIPException
HAS_GEOIP = True
diff --git a/django/contrib/gis/geoip/base.py b/django/contrib/gis/geoip/base.py
index d4793a2ae9..c6e27866db 100644
--- a/django/contrib/gis/geoip/base.py
+++ b/django/contrib/gis/geoip/base.py
@@ -18,7 +18,8 @@ free_regex = re.compile(r'^GEO-\d{3}FREE')
lite_regex = re.compile(r'^GEO-\d{3}LITE')
#### GeoIP classes ####
-class GeoIPException(Exception): pass
+class GeoIPException(Exception):
+ pass
class GeoIP(object):
# The flags for GeoIP memory caching.
diff --git a/django/contrib/gis/geoip/prototypes.py b/django/contrib/gis/geoip/prototypes.py
index 283d721395..c13dd8aba2 100644
--- a/django/contrib/gis/geoip/prototypes.py
+++ b/django/contrib/gis/geoip/prototypes.py
@@ -14,7 +14,7 @@ class GeoIPRecord(Structure):
('longitude', c_float),
# TODO: In 1.4.6 this changed from `int dma_code;` to
# `union {int metro_code; int dma_code;};`. Change
- # to a `ctypes.Union` in to accomodate in future when
+ # to a `ctypes.Union` in to accommodate in future when
# pre-1.4.6 versions are no longer distributed.
('dma_code', c_int),
('area_code', c_int),
diff --git a/django/contrib/gis/geometry/backend/__init__.py b/django/contrib/gis/geometry/backend/__init__.py
index d9f30bb256..5830e482f9 100644
--- a/django/contrib/gis/geometry/backend/__init__.py
+++ b/django/contrib/gis/geometry/backend/__init__.py
@@ -1,11 +1,12 @@
+from importlib import import_module
+
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-from django.utils.importlib import import_module
geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos')
try:
- module = import_module('.%s' % geom_backend, 'django.contrib.gis.geometry.backend')
+ module = import_module('django.contrib.gis.geometry.backend.%s' % geom_backend)
except ImportError:
try:
module = import_module(geom_backend)
diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py
index b088ec2dc4..7021ed1866 100644
--- a/django/contrib/gis/geos/geometry.py
+++ b/django/contrib/gis/geos/geometry.py
@@ -18,7 +18,6 @@ from django.contrib.gis.geos.base import GEOSBase, gdal
from django.contrib.gis.geos.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError
from django.contrib.gis.geos.libgeos import GEOM_PTR, GEOS_PREPARE
-from django.contrib.gis.geos.mutable_list import ListMixin
# All other functions in this module come from the ctypes
# prototypes module -- which handles all interaction with
diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py
index 900a0adb40..87aba723b9 100644
--- a/django/contrib/gis/geos/tests/test_geos.py
+++ b/django/contrib/gis/geos/tests/test_geos.py
@@ -124,24 +124,16 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
self.assertEqual(hexewkb_3d, pnt_3d.hexewkb)
self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz)
else:
- try:
- hexewkb = pnt_3d.hexewkb
- except GEOSException:
- pass
- else:
- self.fail('Should have raised GEOSException.')
+ with self.assertRaises(GEOSException):
+ pnt_3d.hexewkb
# Same for EWKB.
self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb)
if GEOS_PREPARE:
self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb)
else:
- try:
- ewkb = pnt_3d.ewkb
- except GEOSException:
- pass
- else:
- self.fail('Should have raised GEOSException')
+ with self.assertRaises(GEOSException):
+ pnt_3d.ewkb
# Redundant sanity check.
self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid)
@@ -158,7 +150,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
# string-based
for err in self.geometries.errors:
with self.assertRaises((GEOSException, ValueError)):
- _ = fromstr(err.wkt)
+ fromstr(err.wkt)
# Bad WKB
self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0'))
diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py
index 8915f01e50..5e74225f91 100644
--- a/django/contrib/gis/tests/distapp/tests.py
+++ b/django/contrib/gis/tests/distapp/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py
index 6c17003982..6fdf67042b 100644
--- a/django/contrib/gis/tests/geo3d/tests.py
+++ b/django/contrib/gis/tests/geo3d/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import os
import re
diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py
index df4158bb31..295c1de46f 100644
--- a/django/contrib/gis/tests/geoadmin/tests.py
+++ b/django/contrib/gis/tests/geoadmin/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/geoapp/feeds.py b/django/contrib/gis/tests/geoapp/feeds.py
index f53431c24d..9ec959ecce 100644
--- a/django/contrib/gis/tests/geoapp/feeds.py
+++ b/django/contrib/gis/tests/geoapp/feeds.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from django.contrib.gis import feeds
diff --git a/django/contrib/gis/tests/geoapp/sitemaps.py b/django/contrib/gis/tests/geoapp/sitemaps.py
index 0e85fda662..55bf7c764c 100644
--- a/django/contrib/gis/tests/geoapp/sitemaps.py
+++ b/django/contrib/gis/tests/geoapp/sitemaps.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
from django.contrib.gis.sitemaps import GeoRSSSitemap, KMLSitemap, KMZSitemap
from .feeds import feed_dict
diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py
index b2953b4a70..9c7b572e99 100644
--- a/django/contrib/gis/tests/geoapp/test_feeds.py
+++ b/django/contrib/gis/tests/geoapp/test_feeds.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from unittest import skipUnless
from xml.dom import minidom
diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py
index 2ffbaec9a9..844f03aef5 100644
--- a/django/contrib/gis/tests/geoapp/test_regress.py
+++ b/django/contrib/gis/tests/geoapp/test_regress.py
@@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*-
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
from datetime import datetime
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py
index 98cd8cc5ac..bb68039bc3 100644
--- a/django/contrib/gis/tests/geoapp/test_sitemaps.py
+++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from io import BytesIO
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py
index eabc5958c0..9221f99285 100644
--- a/django/contrib/gis/tests/geoapp/tests.py
+++ b/django/contrib/gis/tests/geoapp/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
import re
import unittest
diff --git a/django/contrib/gis/tests/geoapp/urls.py b/django/contrib/gis/tests/geoapp/urls.py
index 55a5fa3670..70db62d71b 100644
--- a/django/contrib/gis/tests/geoapp/urls.py
+++ b/django/contrib/gis/tests/geoapp/urls.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from django.conf.urls import patterns
diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py
index 2a60f623fa..bc809bdfc0 100644
--- a/django/contrib/gis/tests/geogapp/tests.py
+++ b/django/contrib/gis/tests/geogapp/tests.py
@@ -1,7 +1,7 @@
"""
Tests for geography support in PostGIS 1.5+
"""
-from __future__ import absolute_import
+from __future__ import unicode_literals
import os
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/inspectapp/tests.py b/django/contrib/gis/tests/inspectapp/tests.py
index 34dab1ab7d..4b7c5ff21d 100644
--- a/django/contrib/gis/tests/inspectapp/tests.py
+++ b/django/contrib/gis/tests/inspectapp/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
import os
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py
index 3b040624f3..632cb98aeb 100644
--- a/django/contrib/gis/tests/layermap/tests.py
+++ b/django/contrib/gis/tests/layermap/tests.py
@@ -1,5 +1,5 @@
# coding: utf-8
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
from copy import copy
from decimal import Decimal
diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py
index 653bda8aaf..83da000f82 100644
--- a/django/contrib/gis/tests/relatedapp/tests.py
+++ b/django/contrib/gis/tests/relatedapp/tests.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
from unittest import skipUnless
diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py
index 5bb6a70206..498d072926 100644
--- a/django/contrib/gis/tests/test_spatialrefsys.py
+++ b/django/contrib/gis/tests/test_spatialrefsys.py
@@ -24,7 +24,7 @@ test_srs = ({'srid' : 4326,
'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"',
'proj4_re' : r'\+proj=lcc \+lat_1=30.28333333333333 \+lat_2=28.38333333333333 \+lat_0=27.83333333333333 '
r'\+lon_0=-99 \+x_0=600000 \+y_0=4000000 (\+ellps=GRS80 )?'
- r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0)?\+units=m \+no_defs ',
+ r'(\+datum=NAD83 |\+towgs84=0,0,0,0,0,0,0 )?\+units=m \+no_defs ',
'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central',
'geographic' : False, 'projected' : True, 'spatialite' : False,
'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)
diff --git a/django/contrib/messages/__init__.py b/django/contrib/messages/__init__.py
index 68a53d996f..a835f29dc9 100644
--- a/django/contrib/messages/__init__.py
+++ b/django/contrib/messages/__init__.py
@@ -1,4 +1,2 @@
-from __future__ import absolute_import
-
from django.contrib.messages.api import *
from django.contrib.messages.constants import *
diff --git a/django/contrib/sessions/management/commands/clearsessions.py b/django/contrib/sessions/management/commands/clearsessions.py
index 8eb23dfee0..fa0dad31c3 100644
--- a/django/contrib/sessions/management/commands/clearsessions.py
+++ b/django/contrib/sessions/management/commands/clearsessions.py
@@ -1,6 +1,7 @@
+from importlib import import_module
+
from django.conf import settings
from django.core.management.base import NoArgsCommand
-from django.utils.importlib import import_module
class Command(NoArgsCommand):
diff --git a/django/contrib/sessions/middleware.py b/django/contrib/sessions/middleware.py
index 8bc2d37dd3..e8ebf3d5f9 100644
--- a/django/contrib/sessions/middleware.py
+++ b/django/contrib/sessions/middleware.py
@@ -1,9 +1,9 @@
+from importlib import import_module
import time
from django.conf import settings
from django.utils.cache import patch_vary_headers
from django.utils.http import cookie_date
-from django.utils.importlib import import_module
class SessionMiddleware(object):
def __init__(self):
diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py
index 1acae8296c..627813a33e 100644
--- a/django/contrib/sitemaps/__init__.py
+++ b/django/contrib/sitemaps/__init__.py
@@ -86,17 +86,27 @@ class Sitemap(object):
domain = site.domain
urls = []
+ latest_lastmod = None
+ all_items_lastmod = True # track if all items have a lastmod
for item in self.paginator.page(page).object_list:
loc = "%s://%s%s" % (protocol, domain, self.__get('location', item))
priority = self.__get('priority', item, None)
+ lastmod = self.__get('lastmod', item, None)
+ if all_items_lastmod:
+ all_items_lastmod = lastmod is not None
+ if (all_items_lastmod and
+ (latest_lastmod is None or lastmod > latest_lastmod)):
+ latest_lastmod = lastmod
url_info = {
'item': item,
'location': loc,
- 'lastmod': self.__get('lastmod', item, None),
+ 'lastmod': lastmod,
'changefreq': self.__get('changefreq', item, None),
'priority': str(priority if priority is not None else ''),
}
urls.append(url_info)
+ if all_items_lastmod:
+ self.latest_lastmod = latest_lastmod
return urls
class FlatPageSitemap(Sitemap):
diff --git a/django/contrib/sitemaps/tests/test_http.py b/django/contrib/sitemaps/tests/test_http.py
index f68f345815..c870b442de 100644
--- a/django/contrib/sitemaps/tests/test_http.py
+++ b/django/contrib/sitemaps/tests/test_http.py
@@ -77,6 +77,21 @@ class HTTPSitemapTests(SitemapTestsBase):
""" % (self.base_url, date.today())
self.assertXMLEqual(response.content.decode('utf-8'), expected_content)
+ def test_sitemap_last_modified(self):
+ "Tests that Last-Modified header is set correctly"
+ response = self.client.get('/lastmod/sitemap.xml')
+ self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT')
+
+ def test_sitemap_last_modified_missing(self):
+ "Tests that Last-Modified header is missing when sitemap has no lastmod"
+ response = self.client.get('/generic/sitemap.xml')
+ self.assertFalse(response.has_header('Last-Modified'))
+
+ def test_sitemap_last_modified_mixed(self):
+ "Tests that Last-Modified header is omitted when lastmod not on all items"
+ response = self.client.get('/lastmod-mixed/sitemap.xml')
+ self.assertFalse(response.has_header('Last-Modified'))
+
@skipUnless(settings.USE_I18N, "Internationalization is not enabled")
@override_settings(USE_L10N=True)
def test_localized_priority(self):
diff --git a/django/contrib/sitemaps/tests/urls/http.py b/django/contrib/sitemaps/tests/urls/http.py
index a8b804fd4b..6721d72b81 100644
--- a/django/contrib/sitemaps/tests/urls/http.py
+++ b/django/contrib/sitemaps/tests/urls/http.py
@@ -15,10 +15,36 @@ class SimpleSitemap(Sitemap):
def items(self):
return [object()]
+
+class FixedLastmodSitemap(SimpleSitemap):
+ lastmod = datetime(2013, 3, 13, 10, 0, 0)
+
+
+class FixedLastmodMixedSitemap(Sitemap):
+ changefreq = "never"
+ priority = 0.5
+ location = '/location/'
+ loop = 0
+
+ def items(self):
+ o1 = TestModel()
+ o1.lastmod = datetime(2013, 3, 13, 10, 0, 0)
+ o2 = TestModel()
+ return [o1, o2]
+
+
simple_sitemaps = {
'simple': SimpleSitemap,
}
+fixed_lastmod_sitemaps = {
+ 'fixed-lastmod': FixedLastmodSitemap,
+}
+
+fixed_lastmod__mixed_sitemaps = {
+ 'fixed-lastmod-mixed': FixedLastmodMixedSitemap,
+}
+
generic_sitemaps = {
'generic': GenericSitemap({'queryset': TestModel.objects.all()}),
}
@@ -36,6 +62,8 @@ urlpatterns = patterns('django.contrib.sitemaps.views',
(r'^simple/sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps}),
(r'^simple/custom-sitemap\.xml$', 'sitemap',
{'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap.xml'}),
+ (r'^lastmod/sitemap\.xml$', 'sitemap', {'sitemaps': fixed_lastmod_sitemaps}),
+ (r'^lastmod-mixed/sitemap\.xml$', 'sitemap', {'sitemaps': fixed_lastmod__mixed_sitemaps}),
(r'^generic/sitemap\.xml$', 'sitemap', {'sitemaps': generic_sitemaps}),
(r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}),
url(r'^cached/index\.xml$', cache_page(1)(views.index),
diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py
index 7a94fb355d..14983acea9 100644
--- a/django/contrib/sitemaps/views.py
+++ b/django/contrib/sitemaps/views.py
@@ -1,3 +1,4 @@
+from calendar import timegm
from functools import wraps
from django.contrib.sites.models import get_current_site
@@ -6,6 +7,7 @@ from django.core.paginator import EmptyPage, PageNotAnInteger
from django.http import Http404
from django.template.response import TemplateResponse
from django.utils import six
+from django.utils.http import http_date
def x_robots_tag(func):
@wraps(func)
@@ -64,5 +66,11 @@ def sitemap(request, sitemaps, section=None,
raise Http404("Page %s empty" % page)
except PageNotAnInteger:
raise Http404("No page '%s'" % page)
- return TemplateResponse(request, template_name, {'urlset': urls},
- content_type=content_type)
+ response = TemplateResponse(request, template_name, {'urlset': urls},
+ content_type=content_type)
+ if hasattr(site, 'latest_lastmod'):
+ # if latest_lastmod is defined for site, set header so as
+ # ConditionalGetMiddleware is able to send 304 NOT MODIFIED
+ response['Last-Modified'] = http_date(
+ timegm(site.latest_lastmod.utctimetuple()))
+ return response
diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py
index 7d266d95a0..d4efd1a8d8 100644
--- a/django/contrib/staticfiles/finders.py
+++ b/django/contrib/staticfiles/finders.py
@@ -1,8 +1,9 @@
+from collections import OrderedDict
import os
+
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage, Storage, FileSystemStorage
-from django.utils.datastructures import SortedDict
from django.utils.functional import empty, memoize, LazyObject
from django.utils.module_loading import import_by_path
from django.utils._os import safe_join
@@ -11,7 +12,7 @@ from django.utils import six
from django.contrib.staticfiles import utils
from django.contrib.staticfiles.storage import AppStaticStorage
-_finders = SortedDict()
+_finders = OrderedDict()
class BaseFinder(object):
@@ -47,7 +48,7 @@ class FileSystemFinder(BaseFinder):
# List of locations with static files
self.locations = []
# Maps dir paths to an appropriate storage instance
- self.storages = SortedDict()
+ self.storages = OrderedDict()
if not isinstance(settings.STATICFILES_DIRS, (list, tuple)):
raise ImproperlyConfigured(
"Your STATICFILES_DIRS setting is not a tuple or list; "
@@ -118,7 +119,7 @@ class AppDirectoriesFinder(BaseFinder):
# The list of apps that are handled
self.apps = []
# Mapping of app module paths to storage instances
- self.storages = SortedDict()
+ self.storages = OrderedDict()
if apps is None:
apps = settings.INSTALLED_APPS
for app in apps:
diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py
index 7c3de80e93..c1e9fa811b 100644
--- a/django/contrib/staticfiles/management/commands/collectstatic.py
+++ b/django/contrib/staticfiles/management/commands/collectstatic.py
@@ -2,12 +2,12 @@ from __future__ import unicode_literals
import os
import sys
+from collections import OrderedDict
from optparse import make_option
from django.core.files.storage import FileSystemStorage
from django.core.management.base import CommandError, NoArgsCommand
from django.utils.encoding import smart_text
-from django.utils.datastructures import SortedDict
from django.utils.six.moves import input
from django.contrib.staticfiles import finders, storage
@@ -97,7 +97,7 @@ class Command(NoArgsCommand):
else:
handler = self.copy_file
- found_files = SortedDict()
+ found_files = OrderedDict()
for finder in finders.get_finders():
for path, storage in finder.list(self.ignore_patterns):
# Prefix the relative path if the source storage contains it
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index d085cf723f..1242afe411 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -1,5 +1,7 @@
from __future__ import unicode_literals
+from collections import OrderedDict
import hashlib
+from importlib import import_module
import os
import posixpath
import re
@@ -15,10 +17,8 @@ from django.core.cache import (get_cache, InvalidCacheBackendError,
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage, get_storage_class
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes, force_text
from django.utils.functional import LazyObject
-from django.utils.importlib import import_module
from django.utils._os import upath
from django.contrib.staticfiles.utils import check_settings, matches_patterns
@@ -64,7 +64,7 @@ class CachedFilesMixin(object):
except InvalidCacheBackendError:
# Use the default backend
self.cache = default_cache
- self._patterns = SortedDict()
+ self._patterns = OrderedDict()
for extension, patterns in self.patterns:
for pattern in patterns:
if isinstance(pattern, (tuple, list)):
@@ -202,7 +202,7 @@ class CachedFilesMixin(object):
def post_process(self, paths, dry_run=False, **options):
"""
- Post process the given list of files (called from collectstatic).
+ Post process the given OrderedDict of files (called from collectstatic).
Processing is actually two separate operations:
diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py
index f5c42fedf8..7ddc6a1bc2 100644
--- a/django/contrib/staticfiles/views.py
+++ b/django/contrib/staticfiles/views.py
@@ -11,7 +11,6 @@ except ImportError: # Python 2
from urllib import unquote
from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
from django.http import Http404
from django.views import static
@@ -31,9 +30,7 @@ def serve(request, path, insecure=False, **kwargs):
It uses the django.views.static view to serve the found files.
"""
if not settings.DEBUG and not insecure:
- raise ImproperlyConfigured("The staticfiles view can only be used in "
- "debug mode or if the --insecure "
- "option of 'runserver' is used")
+ raise Http404
normalized_path = posixpath.normpath(unquote(path)).lstrip('/')
absolute_path = finders.find(normalized_path)
if not absolute_path:
diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
index 1242372dcf..e1d2eb455a 100644
--- a/django/core/cache/__init__.py
+++ b/django/core/cache/__init__.py
@@ -14,6 +14,7 @@ cache class.
See docs/topics/cache.txt for information on the public API.
"""
+import importlib
try:
from urllib.parse import parse_qsl
except ImportError: # Python 2
@@ -24,7 +25,6 @@ from django.core import signals
from django.core.cache.backends.base import (
InvalidCacheBackendError, CacheKeyWarning, BaseCache)
from django.core.exceptions import ImproperlyConfigured
-from django.utils import importlib
from django.utils.module_loading import import_by_path
diff --git a/django/core/cache/utils.py b/django/core/cache/utils.py
index 4310825ad4..b9806cc5e0 100644
--- a/django/core/cache/utils.py
+++ b/django/core/cache/utils.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import hashlib
from django.utils.encoding import force_bytes
diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py
index 38d8154ac9..967254dfac 100644
--- a/django/core/handlers/wsgi.py
+++ b/django/core/handlers/wsgi.py
@@ -11,7 +11,7 @@ from django.core import signals
from django.core.handlers import base
from django.core.urlresolvers import set_script_prefix
from django.utils import datastructures
-from django.utils.encoding import force_str, force_text, iri_to_uri
+from django.utils.encoding import force_str
# For backwards compatibility -- lots of code uses this in the wild!
from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
index fcff803376..1e2b35cc2f 100644
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -32,7 +32,7 @@ def get_connection(backend=None, fail_silently=False, **kwds):
def send_mail(subject, message, from_email, recipient_list,
fail_silently=False, auth_user=None, auth_password=None,
- connection=None):
+ connection=None, html_message=None):
"""
Easy wrapper for sending a single message to a recipient list. All members
of the recipient list will see the other recipients in the 'To' field.
@@ -46,8 +46,12 @@ def send_mail(subject, message, from_email, recipient_list,
connection = connection or get_connection(username=auth_user,
password=auth_password,
fail_silently=fail_silently)
- return EmailMessage(subject, message, from_email, recipient_list,
- connection=connection).send()
+ mail = EmailMultiAlternatives(subject, message, from_email, recipient_list,
+ connection=connection)
+ if html_message:
+ mail.attach_alternative(html_message, 'text/html')
+
+ return mail.send()
def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py
index 8fd46aa759..2ac797ecfe 100644
--- a/django/core/management/__init__.py
+++ b/django/core/management/__init__.py
@@ -1,13 +1,13 @@
import collections
+import imp
+from importlib import import_module
+from optparse import OptionParser, NO_DEFAULT
import os
import sys
-from optparse import OptionParser, NO_DEFAULT
-import imp
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError, handle_default_options
from django.core.management.color import color_style
-from django.utils.importlib import import_module
from django.utils import six
# For backwards compatibility: get_version() used to be in this module.
@@ -146,7 +146,7 @@ def call_command(name, *args, **options):
# Grab out a list of defaults from the options. optparse does this for us
# when the script runs from the command line, but since call_command can
- # be called programatically, we need to simulate the loading and handling
+ # be called programmatically, we need to simulate the loading and handling
# of defaults (see #10080 for details).
defaults = {}
for opt in klass.option_list:
diff --git a/django/core/management/base.py b/django/core/management/base.py
index af040288d0..9c8940e99f 100644
--- a/django/core/management/base.py
+++ b/django/core/management/base.py
@@ -10,7 +10,7 @@ from optparse import make_option, OptionParser
import django
from django.core.exceptions import ImproperlyConfigured
-from django.core.management.color import color_style
+from django.core.management.color import color_style, no_style
from django.utils.encoding import force_str
from django.utils.six import StringIO
@@ -171,6 +171,8 @@ class BaseCommand(object):
help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
make_option('--traceback', action='store_true',
help='Raise on exception'),
+ make_option('--no-color', action='store_true', dest='no_color', default=False,
+ help="Don't colorize the command output."),
)
help = ''
args = ''
@@ -254,7 +256,11 @@ class BaseCommand(object):
``self.requires_model_validation``, except if force-skipped).
"""
self.stdout = OutputWrapper(options.get('stdout', sys.stdout))
- self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR)
+ if options.get('no_color'):
+ self.style = no_style()
+ self.stderr = OutputWrapper(options.get('stderr', sys.stderr))
+ else:
+ self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR)
if self.can_import_settings:
from django.conf import settings
diff --git a/django/core/management/color.py b/django/core/management/color.py
index 8c7a87fe71..708823705b 100644
--- a/django/core/management/color.py
+++ b/django/core/management/color.py
@@ -30,7 +30,7 @@ def color_style():
class dummy: pass
style = dummy()
# The nocolor palette has all available roles.
- # Use that pallete as the basis for populating
+ # Use that palette as the basis for populating
# the palette as defined in the environment.
for role in termcolors.PALETTES[termcolors.NOCOLOR_PALETTE]:
format = color_settings.get(role,{})
diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py
index 5e440196fc..c74eede846 100644
--- a/django/core/management/commands/dumpdata.py
+++ b/django/core/management/commands/dumpdata.py
@@ -1,10 +1,11 @@
+from collections import OrderedDict
+from optparse import make_option
+
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError
from django.core import serializers
from django.db import router, DEFAULT_DB_ALIAS
-from django.utils.datastructures import SortedDict
-from optparse import make_option
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
@@ -22,7 +23,7 @@ class Command(BaseCommand):
make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False,
help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."),
make_option('--pks', dest='primary_keys', help="Only dump objects with "
- "given primary keys. Accepts a comma seperated list of keys. "
+ "given primary keys. Accepts a comma separated list of keys. "
"This option will only work when you specify one model."),
)
help = ("Output the contents of the database as a fixture of the given "
@@ -66,11 +67,11 @@ class Command(BaseCommand):
if len(app_labels) == 0:
if primary_keys:
raise CommandError("You can only use --pks option with one model")
- app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps)
+ app_list = OrderedDict((app, None) for app in get_apps() if app not in excluded_apps)
else:
if len(app_labels) > 1 and primary_keys:
raise CommandError("You can only use --pks option with one model")
- app_list = SortedDict()
+ app_list = OrderedDict()
for label in app_labels:
try:
app_label, model_label = label.split('.')
diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py
index a6ea45ce95..5e951f97b4 100644
--- a/django/core/management/commands/flush.py
+++ b/django/core/management/commands/flush.py
@@ -1,4 +1,5 @@
import sys
+from importlib import import_module
from optparse import make_option
from django.conf import settings
diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py
index dc26bb11d2..2cfea028ec 100644
--- a/django/core/management/commands/inspectdb.py
+++ b/django/core/management/commands/inspectdb.py
@@ -1,12 +1,12 @@
from __future__ import unicode_literals
+from collections import OrderedDict
import keyword
import re
from optparse import make_option
from django.core.management.base import NoArgsCommand, CommandError
from django.db import connections, DEFAULT_DB_ALIAS
-from django.utils.datastructures import SortedDict
class Command(NoArgsCommand):
@@ -69,7 +69,7 @@ class Command(NoArgsCommand):
used_column_names = [] # Holds column names used in the table so far
for i, row in enumerate(connection.introspection.get_table_description(cursor, table_name)):
comment_notes = [] # Holds Field notes, to be displayed in a Python comment.
- extra_params = SortedDict() # Holds Field parameters such as 'db_column'.
+ extra_params = OrderedDict() # Holds Field parameters such as 'db_column'.
column_name = row[0]
is_relation = i in relations
@@ -193,7 +193,7 @@ class Command(NoArgsCommand):
description, this routine will return the given field type name, as
well as any additional keyword parameters and notes for the field.
"""
- field_params = SortedDict()
+ field_params = OrderedDict()
field_notes = []
try:
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index 802226f9d1..0da36a3c52 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import glob
import gzip
import os
+import warnings
import zipfile
from optparse import make_option
import warnings
@@ -156,12 +157,13 @@ class Command(BaseCommand):
finally:
fixture.close()
- # If the fixture we loaded contains 0 objects, assume that an
- # error was encountered during fixture loading.
+ # Warn if the fixture we loaded contains 0 objects.
if objects_in_fixture == 0:
- raise CommandError(
- "No fixture data found for '%s'. "
- "(File format may be invalid.)" % fixture_name)
+ warnings.warn(
+ "No fixture data found for '%s'. (File format may be "
+ "invalid.)" % fixture_name,
+ RuntimeWarning
+ )
def _find_fixtures(self, fixture_label):
"""
@@ -233,7 +235,7 @@ class Command(BaseCommand):
"""
dirs = []
for path in get_app_paths():
- d = os.path.join(os.path.dirname(path), 'fixtures')
+ d = os.path.join(path, 'fixtures')
if os.path.isdir(d):
dirs.append(d)
dirs.extend(list(settings.FIXTURE_DIRS))
diff --git a/django/core/management/commands/migrate.py b/django/core/management/commands/migrate.py
index 17b5a7dfe9..699b22edaa 100644
--- a/django/core/management/commands/migrate.py
+++ b/django/core/management/commands/migrate.py
@@ -1,4 +1,5 @@
from optparse import make_option
+from collections import OrderedDict
import itertools
import traceback
@@ -10,7 +11,6 @@ from django.core.management.sql import custom_sql_for_model, emit_post_migrate_s
from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
from django.db.migrations.executor import MigrationExecutor
from django.db.migrations.loader import AmbiguityError
-from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
@@ -161,7 +161,7 @@ class Command(BaseCommand):
return not ((converter(opts.db_table) in tables) or
(opts.auto_created and converter(opts.auto_created._meta.db_table) in tables))
- manifest = SortedDict(
+ manifest = OrderedDict(
(app_name, list(filter(model_installed, model_list)))
for app_name, model_list in all_models
)
diff --git a/django/core/management/commands/runfcgi.py b/django/core/management/commands/runfcgi.py
index a60d4ebc59..4e9331fc80 100644
--- a/django/core/management/commands/runfcgi.py
+++ b/django/core/management/commands/runfcgi.py
@@ -1,3 +1,5 @@
+import warnings
+
from django.core.management.base import BaseCommand
class Command(BaseCommand):
@@ -5,6 +7,10 @@ class Command(BaseCommand):
args = '[various KEY=val options, use `runfcgi help` for help]'
def handle(self, *args, **options):
+ warnings.warn(
+ "FastCGI support has been deprecated and will be removed in Django 1.9.",
+ PendingDeprecationWarning)
+
from django.conf import settings
from django.utils import translation
# Activate the current language, because it won't get activated later.
@@ -14,7 +20,7 @@ class Command(BaseCommand):
pass
from django.core.servers.fastcgi import runfastcgi
runfastcgi(args)
-
+
def usage(self, subcommand):
from django.core.servers.fastcgi import FASTCGI_HELP
return FASTCGI_HELP
diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py
index 851d4e3cfb..00a6602c0b 100644
--- a/django/core/management/commands/shell.py
+++ b/django/core/management/commands/shell.py
@@ -19,23 +19,35 @@ class Command(NoArgsCommand):
help = "Runs a Python interactive interpreter. Tries to use IPython or bpython, if one of them is available."
requires_model_validation = False
+ def _ipython_pre_011(self):
+ """Start IPython pre-0.11"""
+ from IPython.Shell import IPShell
+ shell = IPShell(argv=[])
+ shell.mainloop()
+
+ def _ipython_pre_100(self):
+ """Start IPython pre-1.0.0"""
+ from IPython.frontend.terminal.ipapp import TerminalIPythonApp
+ app = TerminalIPythonApp.instance()
+ app.initialize(argv=[])
+ app.start()
+
+ def _ipython(self):
+ """Start IPython >= 1.0"""
+ from IPython import start_ipython
+ start_ipython(argv=[])
+
def ipython(self):
- try:
- from IPython.frontend.terminal.ipapp import TerminalIPythonApp
- app = TerminalIPythonApp.instance()
- app.initialize(argv=[])
- app.start()
- except ImportError:
- # IPython < 0.11
- # Explicitly pass an empty list as arguments, because otherwise
- # IPython would use sys.argv from this script.
+ """Start any version of IPython"""
+ for ip in (self._ipython, self._ipython_pre_100, self._ipython_pre_011):
try:
- from IPython.Shell import IPShell
- shell = IPShell(argv=[])
- shell.mainloop()
+ ip()
except ImportError:
- # IPython not found at all, raise ImportError
- raise
+ pass
+ else:
+ return
+ # no IPython, raise ImportError
+ raise ImportError("No IPython")
def bpython(self):
import bpython
diff --git a/django/core/management/commands/startapp.py b/django/core/management/commands/startapp.py
index 692ad09a43..77e884ca5a 100644
--- a/django/core/management/commands/startapp.py
+++ b/django/core/management/commands/startapp.py
@@ -1,6 +1,7 @@
+from importlib import import_module
+
from django.core.management.base import CommandError
from django.core.management.templates import TemplateCommand
-from django.utils.importlib import import_module
class Command(TemplateCommand):
diff --git a/django/core/management/commands/startproject.py b/django/core/management/commands/startproject.py
index b143e6c380..b7dcbcaba5 100644
--- a/django/core/management/commands/startproject.py
+++ b/django/core/management/commands/startproject.py
@@ -1,7 +1,8 @@
+from importlib import import_module
+
from django.core.management.base import CommandError
from django.core.management.templates import TemplateCommand
from django.utils.crypto import get_random_string
-from django.utils.importlib import import_module
class Command(TemplateCommand):
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 4a61fcddb9..2e977c0c07 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import codecs
import os
import re
+import warnings
from django.conf import settings
from django.core.management.base import CommandError
@@ -168,7 +169,18 @@ def _split_statements(content):
def custom_sql_for_model(model, style, connection):
opts = model._meta
- app_dir = os.path.normpath(os.path.join(os.path.dirname(upath(models.get_app(model._meta.app_label).__file__)), 'sql'))
+ app_dirs = []
+ app_dir = models.get_app_path(model._meta.app_label)
+ app_dirs.append(os.path.normpath(os.path.join(app_dir, 'sql')))
+
+ # Deprecated location -- remove in Django 1.9
+ old_app_dir = os.path.normpath(os.path.join(app_dir, 'models/sql'))
+ if os.path.exists(old_app_dir):
+ warnings.warn("Custom SQL location '<app_label>/models/sql' is "
+ "deprecated, use '<app_label>/sql' instead.",
+ PendingDeprecationWarning)
+ app_dirs.append(old_app_dir)
+
output = []
# Post-creation SQL should come before any initial SQL data is loaded.
@@ -181,8 +193,10 @@ def custom_sql_for_model(model, style, connection):
# Find custom SQL, if it's available.
backend_name = connection.settings_dict['ENGINE'].split('.')[-1]
- sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)),
- os.path.join(app_dir, "%s.sql" % opts.model_name)]
+ sql_files = []
+ for app_dir in app_dirs:
+ sql_files.append(os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)))
+ sql_files.append(os.path.join(app_dir, "%s.sql" % opts.model_name))
for sql_file in sql_files:
if os.path.exists(sql_file):
with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp:
diff --git a/django/core/management/utils.py b/django/core/management/utils.py
index 7159d1123f..d1052d5182 100644
--- a/django/core/management/utils.py
+++ b/django/core/management/utils.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import unicode_literals
import os
from subprocess import PIPE, Popen
diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py
index c48050415d..dc3d139d3b 100644
--- a/django/core/serializers/__init__.py
+++ b/django/core/serializers/__init__.py
@@ -16,8 +16,9 @@ To add your own serializers, use the SERIALIZATION_MODULES setting::
"""
+import importlib
+
from django.conf import settings
-from django.utils import importlib
from django.utils import six
from django.core.serializers.base import SerializerDoesNotExist
diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py
index 64357bf9d5..b07aa3392c 100644
--- a/django/core/serializers/json.py
+++ b/django/core/serializers/json.py
@@ -4,6 +4,7 @@ Serialize data to/from JSON
# Avoid shadowing the standard library json module
from __future__ import absolute_import
+from __future__ import unicode_literals
import datetime
import decimal
diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py
index 2ae1fa5ae3..a612142969 100644
--- a/django/core/servers/fastcgi.py
+++ b/django/core/servers/fastcgi.py
@@ -12,9 +12,9 @@ Run with the extra option "help" for a list of additional options you can
pass to this server.
"""
+import importlib
import os
import sys
-from django.utils import importlib
__version__ = "0.1"
__all__ = ["runfastcgi"]
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
index b7017e47b9..9e086a453a 100644
--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -8,6 +8,7 @@ a string) and returns a tuple in this format:
"""
from __future__ import unicode_literals
+from importlib import import_module
import re
from threading import local
@@ -17,7 +18,6 @@ from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_str, force_text, iri_to_uri
from django.utils.functional import memoize, lazy
from django.utils.http import urlquote
-from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils.regex_helper import normalize
from django.utils import six
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 7185644cc3..ec6081678b 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -9,6 +9,7 @@ except ImportError:
from django.utils.six.moves import _dummy_thread as thread
from collections import namedtuple
from contextlib import contextmanager
+from importlib import import_module
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
@@ -17,7 +18,6 @@ from django.db.backends import util
from django.db.transaction import TransactionManagementError
from django.db.utils import DatabaseErrorWrapper
from django.utils.functional import cached_property
-from django.utils.importlib import import_module
from django.utils import six
from django.utils import timezone
diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py
index 51716a88bd..1d90f03425 100644
--- a/django/db/backends/creation.py
+++ b/django/db/backends/creation.py
@@ -148,7 +148,7 @@ class BaseDatabaseCreation(object):
Returns any ALTER TABLE statements to add constraints after the fact.
"""
opts = model._meta
- if not opts.managed or opts.proxy or opts.swapped:
+ if not opts.managed or opts.swapped:
return []
qn = self.connection.ops.quote_name
final_output = []
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 63d38bdafd..46022a97b1 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -579,7 +579,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s"
% self._standard_operators['contains'],
['X'])
- except utils.DatabaseError:
+ except DatabaseError:
self.operators = self._likec_operators
else:
self.operators = self._standard_operators
diff --git a/django/db/backends/oracle/compiler.py b/django/db/backends/oracle/compiler.py
index d2d4cd3ac9..b6eb80e7ce 100644
--- a/django/db/backends/oracle/compiler.py
+++ b/django/db/backends/oracle/compiler.py
@@ -25,7 +25,7 @@ class SQLCompiler(compiler.SQLCompiler):
def as_sql(self, with_limits=True, with_col_aliases=False):
"""
Creates the SQL for this query. Returns the SQL string and list
- of parameters. This is overriden from the original Query class
+ of parameters. This is overridden from the original Query class
to handle the additional SQL Oracle requires to emulate LIMIT
and OFFSET.
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 960b6def03..841ca92f3e 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -338,7 +338,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
if 'check_same_thread' in kwargs and kwargs['check_same_thread']:
warnings.warn(
'The `check_same_thread` option was provided and set to '
- 'True. It will be overriden with False. Use the '
+ 'True. It will be overridden with False. Use the '
'`DatabaseWrapper.allow_thread_sharing` property instead '
'for controlling thread shareability.',
RuntimeWarning
diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py
index 4d310e480b..2ee525faf1 100644
--- a/django/db/models/__init__.py
+++ b/django/db/models/__init__.py
@@ -1,8 +1,8 @@
from functools import wraps
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
-from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp
-from django.db.models.query import Q
+from django.db.models.loading import get_apps, get_app_path, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp
+from django.db.models.query import Q, QuerySet
from django.db.models.expressions import F
from django.db.models.manager import Manager
from django.db.models.base import Model
diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py
index e0bfb9d879..f4c64f7b7d 100644
--- a/django/db/models/deletion.py
+++ b/django/db/models/deletion.py
@@ -1,8 +1,8 @@
+from collections import OrderedDict
from operator import attrgetter
from django.db import connections, transaction, IntegrityError
from django.db.models import signals, sql
-from django.utils.datastructures import SortedDict
from django.utils import six
@@ -234,7 +234,7 @@ class Collector(object):
found = True
if not found:
return
- self.data = SortedDict([(model, self.data[model])
+ self.data = OrderedDict([(model, self.data[model])
for model in sorted_models])
def delete(self):
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 0410815c92..4135c60ad3 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -17,7 +17,7 @@ from django import forms
from django.core import exceptions, validators
from django.utils.datastructures import DictWrapper
from django.utils.dateparse import parse_date, parse_datetime, parse_time
-from django.utils.functional import curry, total_ordering
+from django.utils.functional import curry, total_ordering, Promise
from django.utils.text import capfirst
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
@@ -90,7 +90,7 @@ class Field(object):
'already exists.'),
}
- # Generic field type description, usually overriden by subclasses
+ # Generic field type description, usually overridden by subclasses
def _description(self):
return _('Field of type: %(field_type)s') % {
'field_type': self.__class__.__name__
@@ -441,6 +441,8 @@ class Field(object):
"""
Perform preliminary non-db specific value checks and conversions.
"""
+ if isinstance(value, Promise):
+ value = value._proxy____cast()
return value
def get_db_prep_value(self, value, connection, prepared=False):
@@ -562,7 +564,14 @@ class Field(object):
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
"""Returns choices with a default blank choices included, for use
as SelectField choices for this field."""
- first_choice = blank_choice if include_blank else []
+ blank_defined = False
+ for choice, _ in self.choices:
+ if choice in ('', None):
+ blank_defined = True
+ break
+
+ first_choice = (blank_choice if include_blank and
+ not blank_defined else [])
if self.choices:
return first_choice + list(self.choices)
rel_model = self.rel.to
@@ -724,6 +733,7 @@ class AutoField(Field):
return value
def get_prep_value(self, value):
+ value = super(AutoField, self).get_prep_value(value)
if value is None:
return None
return int(value)
@@ -783,6 +793,7 @@ class BooleanField(Field):
return super(BooleanField, self).get_prep_lookup(lookup_type, value)
def get_prep_value(self, value):
+ value = super(BooleanField, self).get_prep_value(value)
if value is None:
return None
return bool(value)
@@ -816,6 +827,7 @@ class CharField(Field):
return smart_text(value)
def get_prep_value(self, value):
+ value = super(CharField, self).get_prep_value(value)
return self.to_python(value)
def formfield(self, **kwargs):
@@ -931,6 +943,7 @@ class DateField(Field):
return super(DateField, self).get_prep_lookup(lookup_type, value)
def get_prep_value(self, value):
+ value = super(DateField, self).get_prep_value(value)
return self.to_python(value)
def get_db_prep_value(self, value, connection, prepared=False):
@@ -1028,6 +1041,7 @@ class DateTimeField(DateField):
# get_prep_lookup is inherited from DateField
def get_prep_value(self, value):
+ value = super(DateTimeField, self).get_prep_value(value)
value = self.to_python(value)
if value is not None and settings.USE_TZ and timezone.is_naive(value):
# For backwards compatibility, interpret naive datetimes in local
@@ -1116,6 +1130,7 @@ class DecimalField(Field):
self.max_digits, self.decimal_places)
def get_prep_value(self, value):
+ value = super(DecimalField, self).get_prep_value(value)
return self.to_python(value)
def formfield(self, **kwargs):
@@ -1205,6 +1220,7 @@ class FloatField(Field):
description = _("Floating point number")
def get_prep_value(self, value):
+ value = super(FloatField, self).get_prep_value(value)
if value is None:
return None
return float(value)
@@ -1238,6 +1254,7 @@ class IntegerField(Field):
description = _("Integer")
def get_prep_value(self, value):
+ value = super(IntegerField, self).get_prep_value(value)
if value is None:
return None
return int(value)
@@ -1346,6 +1363,7 @@ class GenericIPAddressField(Field):
return value or None
def get_prep_value(self, value):
+ value = super(GenericIPAddressField, self).get_prep_value(value)
if value and ':' in value:
try:
return clean_ipv6_address(value, self.unpack_ipv4)
@@ -1411,6 +1429,7 @@ class NullBooleanField(Field):
value)
def get_prep_value(self, value):
+ value = super(NullBooleanField, self).get_prep_value(value)
if value is None:
return None
return bool(value)
@@ -1493,6 +1512,7 @@ class TextField(Field):
return "TextField"
def get_prep_value(self, value):
+ value = super(TextField, self).get_prep_value(value)
if isinstance(value, six.string_types) or value is None:
return value
return smart_text(value)
@@ -1569,6 +1589,7 @@ class TimeField(Field):
return super(TimeField, self).pre_save(model_instance, add)
def get_prep_value(self, value):
+ value = super(TimeField, self).get_prep_value(value)
return self.to_python(value)
def get_db_prep_value(self, value, connection, prepared=False):
diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py
index 311f74a905..61e3eebf49 100644
--- a/django/db/models/fields/files.py
+++ b/django/db/models/fields/files.py
@@ -253,6 +253,7 @@ class FileField(Field):
def get_prep_value(self, value):
"Returns field's value prepared for saving into a database."
+ value = super(FileField, self).get_prep_value(value)
# Need to convert File objects provided via a form to unicode for database insertion
if value is None:
return None
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 5a7718e880..00da186279 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -294,10 +294,15 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec
params = dict(
(rh_field.attname, getattr(instance, lh_field.attname))
for lh_field, rh_field in self.field.related_fields)
- params.update(self.field.get_extra_descriptor_filter(instance))
qs = self.get_queryset(instance=instance)
+ extra_filter = self.field.get_extra_descriptor_filter(instance)
+ if isinstance(extra_filter, dict):
+ params.update(extra_filter)
+ qs = qs.filter(**params)
+ else:
+ qs = qs.filter(extra_filter, **params)
# Assuming the database enforces foreign keys, this won't fail.
- rel_obj = qs.get(**params)
+ rel_obj = qs.get()
if not self.field.rel.multiple:
setattr(rel_obj, self.field.related.get_cache_name(), instance)
setattr(instance, self.cache_name, rel_obj)
@@ -1003,10 +1008,11 @@ class ForeignObject(RelatedField):
user does 'instance.fieldname', that is the extra filter is used in
the descriptor of the field.
- The filter should be something usable in .filter(**kwargs) call, and
- will be ANDed together with the joining columns condition.
+ The filter should be either a dict usable in .filter(**kwargs) call or
+ a Q-object. The condition will be ANDed together with the relation's
+ joining columns.
- A parallel method is get_extra_relation_restriction() which is used in
+ A parallel method is get_extra_restriction() which is used in
JOIN and subquery conditions.
"""
return {}
@@ -1054,7 +1060,7 @@ class ForeignObject(RelatedField):
value_list = []
for source in sources:
# Account for one-to-one relations when sent a different model
- while not isinstance(value, source.model):
+ while not isinstance(value, source.model) and source.rel:
source = source.rel.to._meta.get_field(source.rel.field_name)
value_list.append(getattr(value, source.attname))
return tuple(value_list)
@@ -1119,7 +1125,7 @@ class ForeignObject(RelatedField):
class ForeignKey(ForeignObject):
empty_strings_allowed = False
default_error_messages = {
- 'invalid': _('Model %(model)s with pk %(pk)r does not exist.')
+ 'invalid': _('%(model)s instance with pk %(pk)r does not exist.')
}
description = _("Foreign Key (type determined by related field)")
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index df3be5fe9b..2858b8b699 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -1,22 +1,31 @@
"Utilities for loading models and the modules that contain them."
+from collections import OrderedDict
+import copy
+import imp
+from importlib import import_module
+import os
+import sys
+
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-from django.utils.datastructures import SortedDict
-from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils._os import upath
from django.utils import six
-import imp
-import sys
-import os
-
__all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models',
'load_app', 'app_cache_ready')
MODELS_MODULE_NAME = 'models'
+class ModelDict(OrderedDict):
+ """
+ We need to special-case the deepcopy for this, as the keys are modules,
+ which can't be deep copied.
+ """
+ def __deepcopy__(self, memo):
+ return self.__class__([(key, copy.deepcopy(value, memo))
+ for key, value in self.items()])
class UnavailableApp(Exception):
pass
@@ -29,14 +38,14 @@ def _initialize():
"""
return dict(
# Keys of app_store are the model modules for each application.
- app_store = SortedDict(),
+ app_store=ModelDict(),
# Mapping of installed app_labels to model modules for that app.
app_labels = {},
# Mapping of app_labels to a dictionary of model names to model code.
# May contain apps that are not installed.
- app_models = SortedDict(),
+ app_models=ModelDict(),
# Mapping of app_labels to errors raised when trying to import the app.
app_errors = {},
@@ -118,11 +127,11 @@ class BaseAppCache(object):
Loads the app with the provided fully qualified name, and returns the
model module.
"""
+ app_module = import_module(app_name)
self.handled.add(app_name)
self.nesting_level += 1
- app_module = import_module(app_name)
try:
- models = import_module('.' + MODELS_MODULE_NAME, app_name)
+ models = import_module('%s.%s' % (app_name, MODELS_MODULE_NAME))
except ImportError:
self.nesting_level -= 1
# If the app doesn't have a models module, we can just ignore the
@@ -176,6 +185,16 @@ class BaseAppCache(object):
return [elt[0] for elt in apps]
+ def _get_app_path(self, app):
+ if hasattr(app, '__path__'): # models/__init__.py package
+ app_path = app.__path__[0]
+ else: # models.py module
+ app_path = app.__file__
+ return os.path.dirname(upath(app_path))
+
+ def get_app_path(self, app_label):
+ return self._get_app_path(self.get_app(app_label))
+
def get_app_paths(self):
"""
Returns a list of paths to all installed apps.
@@ -187,10 +206,7 @@ class BaseAppCache(object):
app_paths = []
for app in self.get_apps():
- if hasattr(app, '__path__'): # models/__init__.py package
- app_paths.extend([upath(path) for path in app.__path__])
- else: # models.py module
- app_paths.append(upath(app.__file__))
+ app_paths.append(self._get_app_path(app))
return app_paths
def get_app(self, app_label, emptyOK=False):
@@ -262,12 +278,12 @@ class BaseAppCache(object):
if app_mod:
if app_mod in self.app_store:
app_list = [self.app_models.get(self._label_for(app_mod),
- SortedDict())]
+ ModelDict())]
else:
app_list = []
else:
if only_installed:
- app_list = [self.app_models.get(app_label, SortedDict())
+ app_list = [self.app_models.get(app_label, ModelDict())
for app_label in six.iterkeys(self.app_labels)]
else:
app_list = six.itervalues(self.app_models)
@@ -318,7 +334,7 @@ class BaseAppCache(object):
# Store as 'name: model' pair in a dictionary
# in the app_models dictionary
model_name = model._meta.model_name
- model_dict = self.app_models.setdefault(app_label, SortedDict())
+ model_dict = self.app_models.setdefault(app_label, ModelDict())
if model_name in model_dict:
# The same model may be imported via different paths (e.g.
# appname.models and project.appname.models). We use the source
@@ -364,6 +380,7 @@ cache = AppCache()
# These methods were always module level, so are kept that way for backwards
# compatibility.
get_apps = cache.get_apps
+get_app_path = cache.get_app_path
get_app_paths = cache.get_app_paths
get_app = cache.get_app
get_app_errors = cache.get_app_errors
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index b369aedb64..4f16b5ebfe 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -1,6 +1,8 @@
import copy
+import inspect
+
from django.db import router
-from django.db.models.query import QuerySet, insert_query, RawQuerySet
+from django.db.models.query import QuerySet
from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
from django.utils import six
@@ -56,17 +58,51 @@ class RenameManagerMethods(RenameMethodsBase):
)
-class Manager(six.with_metaclass(RenameManagerMethods)):
+class BaseManager(six.with_metaclass(RenameManagerMethods)):
# Tracks each time a Manager instance is created. Used to retain order.
creation_counter = 0
def __init__(self):
- super(Manager, self).__init__()
+ super(BaseManager, self).__init__()
self._set_creation_counter()
self.model = None
self._inherited = False
self._db = None
+ @classmethod
+ def _get_queryset_methods(cls, queryset_class):
+ def create_method(name, method):
+ def manager_method(self, *args, **kwargs):
+ return getattr(self.get_queryset(), name)(*args, **kwargs)
+ manager_method.__name__ = method.__name__
+ manager_method.__doc__ = method.__doc__
+ return manager_method
+
+ new_methods = {}
+ # Refs http://bugs.python.org/issue1785.
+ predicate = inspect.isfunction if six.PY3 else inspect.ismethod
+ for name, method in inspect.getmembers(queryset_class, predicate=predicate):
+ # Only copy missing methods.
+ if hasattr(cls, name):
+ continue
+ # Only copy public methods or methods with the attribute `queryset_only=False`.
+ queryset_only = getattr(method, 'queryset_only', None)
+ if queryset_only or (queryset_only is None and name.startswith('_')):
+ continue
+ # Copy the method onto the manager.
+ new_methods[name] = create_method(name, method)
+ return new_methods
+
+ @classmethod
+ def from_queryset(cls, queryset_class, class_name=None):
+ if class_name is None:
+ class_name = '%sFrom%s' % (cls.__name__, queryset_class.__name__)
+ class_dict = {
+ '_queryset_class': queryset_class,
+ }
+ class_dict.update(cls._get_queryset_methods(queryset_class))
+ return type(class_name, (cls,), class_dict)
+
def contribute_to_class(self, model, name):
# TODO: Use weakref because of possible memory leak / circular reference.
self.model = model
@@ -92,8 +128,8 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
Sets the creation counter value for this instance and increments the
class-level copy.
"""
- self.creation_counter = Manager.creation_counter
- Manager.creation_counter += 1
+ self.creation_counter = BaseManager.creation_counter
+ BaseManager.creation_counter += 1
def _copy_to_model(self, model):
"""
@@ -117,129 +153,23 @@ class Manager(six.with_metaclass(RenameManagerMethods)):
def db(self):
return self._db or router.db_for_read(self.model)
- #######################
- # PROXIES TO QUERYSET #
- #######################
-
def get_queryset(self):
- """Returns a new QuerySet object. Subclasses can override this method
- to easily customize the behavior of the Manager.
"""
- return QuerySet(self.model, using=self._db)
-
- def none(self):
- return self.get_queryset().none()
+ Returns a new QuerySet object. Subclasses can override this method to
+ easily customize the behavior of the Manager.
+ """
+ return self._queryset_class(self.model, using=self._db)
def all(self):
+ # We can't proxy this method through the `QuerySet` like we do for the
+ # rest of the `QuerySet` methods. This is because `QuerySet.all()`
+ # works by creating a "copy" of the current queryset and in making said
+ # copy, all the cached `prefetch_related` lookups are lost. See the
+ # implementation of `RelatedManager.get_queryset()` for a better
+ # understanding of how this comes into play.
return self.get_queryset()
- def count(self):
- return self.get_queryset().count()
-
- def dates(self, *args, **kwargs):
- return self.get_queryset().dates(*args, **kwargs)
-
- def datetimes(self, *args, **kwargs):
- return self.get_queryset().datetimes(*args, **kwargs)
-
- def distinct(self, *args, **kwargs):
- return self.get_queryset().distinct(*args, **kwargs)
-
- def extra(self, *args, **kwargs):
- return self.get_queryset().extra(*args, **kwargs)
-
- def get(self, *args, **kwargs):
- return self.get_queryset().get(*args, **kwargs)
-
- def get_or_create(self, **kwargs):
- return self.get_queryset().get_or_create(**kwargs)
-
- def update_or_create(self, **kwargs):
- return self.get_queryset().update_or_create(**kwargs)
-
- def create(self, **kwargs):
- return self.get_queryset().create(**kwargs)
-
- def bulk_create(self, *args, **kwargs):
- return self.get_queryset().bulk_create(*args, **kwargs)
-
- def filter(self, *args, **kwargs):
- return self.get_queryset().filter(*args, **kwargs)
-
- def aggregate(self, *args, **kwargs):
- return self.get_queryset().aggregate(*args, **kwargs)
-
- def annotate(self, *args, **kwargs):
- return self.get_queryset().annotate(*args, **kwargs)
-
- def complex_filter(self, *args, **kwargs):
- return self.get_queryset().complex_filter(*args, **kwargs)
-
- def exclude(self, *args, **kwargs):
- return self.get_queryset().exclude(*args, **kwargs)
-
- def in_bulk(self, *args, **kwargs):
- return self.get_queryset().in_bulk(*args, **kwargs)
-
- def iterator(self, *args, **kwargs):
- return self.get_queryset().iterator(*args, **kwargs)
-
- def earliest(self, *args, **kwargs):
- return self.get_queryset().earliest(*args, **kwargs)
-
- def latest(self, *args, **kwargs):
- return self.get_queryset().latest(*args, **kwargs)
-
- def first(self):
- return self.get_queryset().first()
-
- def last(self):
- return self.get_queryset().last()
-
- def order_by(self, *args, **kwargs):
- return self.get_queryset().order_by(*args, **kwargs)
-
- def select_for_update(self, *args, **kwargs):
- return self.get_queryset().select_for_update(*args, **kwargs)
-
- def select_related(self, *args, **kwargs):
- return self.get_queryset().select_related(*args, **kwargs)
-
- def prefetch_related(self, *args, **kwargs):
- return self.get_queryset().prefetch_related(*args, **kwargs)
-
- def values(self, *args, **kwargs):
- return self.get_queryset().values(*args, **kwargs)
-
- def values_list(self, *args, **kwargs):
- return self.get_queryset().values_list(*args, **kwargs)
-
- def update(self, *args, **kwargs):
- return self.get_queryset().update(*args, **kwargs)
-
- def reverse(self, *args, **kwargs):
- return self.get_queryset().reverse(*args, **kwargs)
-
- def defer(self, *args, **kwargs):
- return self.get_queryset().defer(*args, **kwargs)
-
- def only(self, *args, **kwargs):
- return self.get_queryset().only(*args, **kwargs)
-
- def using(self, *args, **kwargs):
- return self.get_queryset().using(*args, **kwargs)
-
- def exists(self, *args, **kwargs):
- return self.get_queryset().exists(*args, **kwargs)
-
- def _insert(self, objs, fields, **kwargs):
- return insert_query(self.model, objs, fields, **kwargs)
-
- def _update(self, values, **kwargs):
- return self.get_queryset()._update(values, **kwargs)
-
- def raw(self, raw_query, params=None, *args, **kwargs):
- return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs)
+Manager = BaseManager.from_queryset(QuerySet, class_name='Manager')
class ManagerDescriptor(object):
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 1cacbc776c..e6901fb297 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+from collections import OrderedDict
import re
from bisect import bisect
import warnings
@@ -11,7 +12,6 @@ from django.db.models.fields.proxy import OrderWrt
from django.db.models.loading import get_models, app_cache_ready, cache
from django.utils import six
from django.utils.functional import cached_property
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible
from django.utils.translation import activate, deactivate_all, get_language, string_concat
@@ -40,7 +40,6 @@ class Options(object):
self.get_latest_by = None
self.order_with_respect_to = None
self.db_tablespace = settings.DEFAULT_TABLESPACE
- self.admin = None
self.meta = meta
self.pk = None
self.has_auto_field, self.auto_field = False, None
@@ -58,7 +57,7 @@ class Options(object):
# concrete models, the concrete_model is always the class itself.
self.concrete_model = None
self.swappable = None
- self.parents = SortedDict()
+ self.parents = OrderedDict()
self.auto_created = False
# To handle various inheritance situations, we need to track where
@@ -212,9 +211,10 @@ class Options(object):
def pk_index(self):
"""
- Returns the index of the primary key field in the self.fields list.
+ Returns the index of the primary key field in the self.concrete_fields
+ list.
"""
- return self.fields.index(self.pk)
+ return self.concrete_fields.index(self.pk)
def setup_proxy(self, target):
"""
@@ -340,7 +340,7 @@ class Options(object):
return list(six.iteritems(self._m2m_cache))
def _fill_m2m_cache(self):
- cache = SortedDict()
+ cache = OrderedDict()
for parent in self.parents:
for field, model in parent._meta.get_m2m_with_model():
if model:
@@ -411,12 +411,13 @@ class Options(object):
for f, model in self.get_all_related_objects_with_model():
cache[f.field.related_query_name()] = (f, model, False, False)
for f, model in self.get_m2m_with_model():
- cache[f.name] = (f, model, True, True)
+ cache[f.name] = cache[f.attname] = (f, model, True, True)
for f, model in self.get_fields_with_model():
- cache[f.name] = (f, model, True, False)
+ cache[f.name] = cache[f.attname] = (f, model, True, False)
for f in self.virtual_fields:
if hasattr(f, 'related'):
- cache[f.name] = (f.related, None if f.model == self.model else f.model, True, False)
+ cache[f.name] = cache[f.attname] = (
+ f.related, None if f.model == self.model else f.model, True, False)
if app_cache_ready():
self._name_map = cache
return cache
@@ -481,7 +482,7 @@ class Options(object):
return [t for t in cache.items() if all(p(*t) for p in predicates)]
def _fill_related_objects_cache(self):
- cache = SortedDict()
+ cache = OrderedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_objects_with_model(include_hidden=True):
@@ -526,7 +527,7 @@ class Options(object):
return list(six.iteritems(cache))
def _fill_related_many_to_many_cache(self):
- cache = SortedDict()
+ cache = OrderedDict()
parent_list = self.get_parent_list()
for parent in self.parents:
for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 811e917764..4069c04a14 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -8,9 +8,9 @@ import sys
from django.conf import settings
from django.core import exceptions
-from django.db import connections, router, transaction, DatabaseError
+from django.db import connections, router, transaction, DatabaseError, IntegrityError
from django.db.models.constants import LOOKUP_SEP
-from django.db.models.fields import AutoField
+from django.db.models.fields import AutoField, Empty
from django.db.models.query_utils import (Q, select_related_descend,
deferred_class_factory, InvalidQuery)
from django.db.models.deletion import Collector
@@ -30,10 +30,23 @@ REPR_OUTPUT_SIZE = 20
EmptyResultSet = sql.EmptyResultSet
+def _pickle_queryset(class_bases, class_dict):
+ """
+ Used by `__reduce__` to create the initial version of the `QuerySet` class
+ onto which the output of `__getstate__` will be applied.
+
+ See `__reduce__` for more details.
+ """
+ new = Empty()
+ new.__class__ = type(class_bases[0].__name__, class_bases, class_dict)
+ return new
+
+
class QuerySet(object):
"""
Represents a lazy database lookup for a set of objects.
"""
+
def __init__(self, model=None, query=None, using=None):
self.model = model
self._db = using
@@ -45,6 +58,13 @@ class QuerySet(object):
self._prefetch_done = False
self._known_related_objects = {} # {rel_field, {pk: rel_obj}}
+ def as_manager(cls):
+ # Address the circular dependency between `Queryset` and `Manager`.
+ from django.db.models.manager import Manager
+ return Manager.from_queryset(cls)()
+ as_manager.queryset_only = True
+ as_manager = classmethod(as_manager)
+
########################
# PYTHON MAGIC METHODS #
########################
@@ -70,6 +90,26 @@ class QuerySet(object):
obj_dict = self.__dict__.copy()
return obj_dict
+ def __reduce__(self):
+ """
+ Used by pickle to deal with the types that we create dynamically when
+ specialized queryset such as `ValuesQuerySet` are used in conjunction
+ with querysets that are *subclasses* of `QuerySet`.
+
+ See `_clone` implementation for more details.
+ """
+ if hasattr(self, '_specialized_queryset_class'):
+ class_bases = (
+ self._specialized_queryset_class,
+ self._base_queryset_class,
+ )
+ class_dict = {
+ '_specialized_queryset_class': self._specialized_queryset_class,
+ '_base_queryset_class': self._base_queryset_class,
+ }
+ return _pickle_queryset, (class_bases, class_dict), self.__getstate__()
+ return super(QuerySet, self).__reduce__()
+
def __repr__(self):
data = list(self[:REPR_OUTPUT_SIZE + 1])
if len(data) > REPR_OUTPUT_SIZE:
@@ -274,9 +314,11 @@ class QuerySet(object):
query = self.query.clone()
+ aggregate_names = []
for (alias, aggregate_expr) in kwargs.items():
query.add_aggregate(aggregate_expr, self.model, alias,
- is_summary=True)
+ is_summary=True)
+ aggregate_names.append(alias)
return query.get_aggregation(using=self.db)
@@ -371,8 +413,8 @@ class QuerySet(object):
specifying whether an object was created.
"""
lookup, params, _ = self._extract_model_params(defaults, **kwargs)
+ self._for_write = True
try:
- self._for_write = True
return self.get(**lookup), False
except self.model.DoesNotExist:
return self._create_object_from_params(lookup, params)
@@ -385,8 +427,8 @@ class QuerySet(object):
specifying whether an object was created.
"""
lookup, params, filtered_defaults = self._extract_model_params(defaults, **kwargs)
+ self._for_write = True
try:
- self._for_write = True
obj = self.get(**lookup)
except self.model.DoesNotExist:
obj, created = self._create_object_from_params(lookup, params)
@@ -394,34 +436,36 @@ class QuerySet(object):
return obj, created
for k, v in six.iteritems(filtered_defaults):
setattr(obj, k, v)
+
+ sid = transaction.savepoint(using=self.db)
try:
- sid = transaction.savepoint(using=self.db)
obj.save(update_fields=filtered_defaults.keys(), using=self.db)
transaction.savepoint_commit(sid, using=self.db)
return obj, False
except DatabaseError:
transaction.savepoint_rollback(sid, using=self.db)
- six.reraise(sys.exc_info())
+ six.reraise(*sys.exc_info())
def _create_object_from_params(self, lookup, params):
"""
Tries to create an object using passed params.
Used by get_or_create and update_or_create
"""
+ obj = self.model(**params)
+ sid = transaction.savepoint(using=self.db)
try:
- obj = self.model(**params)
- sid = transaction.savepoint(using=self.db)
obj.save(force_insert=True, using=self.db)
transaction.savepoint_commit(sid, using=self.db)
return obj, True
- except DatabaseError:
+ except DatabaseError as e:
transaction.savepoint_rollback(sid, using=self.db)
exc_info = sys.exc_info()
- try:
- return self.get(**lookup), False
- except self.model.DoesNotExist:
- # Re-raise the DatabaseError with its original traceback.
- six.reraise(*exc_info)
+ if isinstance(e, IntegrityError):
+ try:
+ return self.get(**lookup), False
+ except self.model.DoesNotExist:
+ pass
+ six.reraise(*exc_info)
def _extract_model_params(self, defaults, **kwargs):
"""
@@ -523,6 +567,7 @@ class QuerySet(object):
# Clear the result cache, in case this QuerySet gets reused.
self._result_cache = None
delete.alters_data = True
+ delete.queryset_only = True
def _raw_delete(self, using):
"""
@@ -562,6 +607,7 @@ class QuerySet(object):
self._result_cache = None
return query.get_compiler(self.db).execute_sql(None)
_update.alters_data = True
+ _update.queryset_only = False
def exists(self):
if self._result_cache is None:
@@ -577,6 +623,13 @@ class QuerySet(object):
# PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
##################################################
+ def raw(self, raw_query, params=None, translations=None, using=None):
+ if using is None:
+ using = self.db
+ return RawQuerySet(raw_query, model=self.model,
+ params=params, translations=translations,
+ using=using)
+
def values(self, *fields):
return self._clone(klass=ValuesQuerySet, setup=True, _fields=fields)
@@ -689,6 +742,7 @@ class QuerySet(object):
# Default to false for nowait
nowait = kwargs.pop('nowait', False)
obj = self._clone()
+ obj._for_write = True
obj.query.select_for_update = True
obj.query.select_for_update_nowait = nowait
return obj
@@ -863,6 +917,21 @@ class QuerySet(object):
###################
# PRIVATE METHODS #
###################
+
+ def _insert(self, objs, fields, return_id=False, raw=False, using=None):
+ """
+ Inserts a new record for the given model. This provides an interface to
+ the InsertQuery class and is how Model.save() is implemented.
+ """
+ self._for_write = True
+ if using is None:
+ using = self.db
+ query = sql.InsertQuery(self.model)
+ query.insert_values(fields, objs, raw=raw)
+ return query.get_compiler(using=using).execute_sql(return_id)
+ _insert.alters_data = True
+ _insert.queryset_only = False
+
def _batched_insert(self, objs, fields, batch_size):
"""
A little helper method for bulk_insert to insert the bulk one batch
@@ -881,6 +950,15 @@ class QuerySet(object):
def _clone(self, klass=None, setup=False, **kwargs):
if klass is None:
klass = self.__class__
+ elif not issubclass(self.__class__, klass):
+ base_queryset_class = getattr(self, '_base_queryset_class', self.__class__)
+ class_bases = (klass, base_queryset_class)
+ class_dict = {
+ '_base_queryset_class': base_queryset_class,
+ '_specialized_queryset_class': klass,
+ }
+ klass = type(klass.__name__, class_bases, class_dict)
+
query = self.query.clone()
if self._sticky_filter:
query.filter_is_sticky = True
@@ -1546,17 +1624,6 @@ class RawQuerySet(object):
return self._model_fields
-def insert_query(model, objs, fields, return_id=False, raw=False, using=None):
- """
- Inserts a new record for the given model. This provides an interface to
- the InsertQuery class and is how Model.save() is implemented. It is not
- part of the public API.
- """
- query = sql.InsertQuery(model)
- query.insert_values(fields, objs, raw=raw)
- return query.get_compiler(using=using).execute_sql(return_id)
-
-
def prefetch_related_objects(result_cache, related_lookups):
"""
Helper function for prefetch_related functionality
diff --git a/django/db/models/sql/__init__.py b/django/db/models/sql/__init__.py
index df5b74e326..8bc60c1d10 100644
--- a/django/db/models/sql/__init__.py
+++ b/django/db/models/sql/__init__.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.sql.subqueries import *
from django.db.models.sql.query import *
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 2a9b8ef826..15f2643495 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -7,9 +7,9 @@ databases). The abstraction barrier only works one way: this module has to know
all about the internals of models in order to get the information it needs.
"""
+from collections import OrderedDict
import copy
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_text
from django.utils.tree import Node
from django.utils import six
@@ -142,7 +142,7 @@ class Query(object):
self.select_related = False
# SQL aggregate-related attributes
- self.aggregates = SortedDict() # Maps alias -> SQL aggregate function
+ self.aggregates = OrderedDict() # Maps alias -> SQL aggregate function
self.aggregate_select_mask = None
self._aggregate_select_cache = None
@@ -152,7 +152,7 @@ class Query(object):
# These are for extensions. The contents are more or less appended
# verbatim to the appropriate clause.
- self.extra = SortedDict() # Maps col_alias -> (col_sql, params).
+ self.extra = OrderedDict() # Maps col_alias -> (col_sql, params).
self.extra_select_mask = None
self._extra_select_cache = None
@@ -321,6 +321,7 @@ class Query(object):
# information but retrieves only the first row. Aggregate
# over the subquery instead.
if self.group_by is not None:
+
from django.db.models.sql.subqueries import AggregateQuery
query = AggregateQuery(self.model)
@@ -479,7 +480,7 @@ class Query(object):
rhs_used_joins = set(change_map.values())
to_promote = [alias for alias in self.tables
if alias not in rhs_used_joins]
- self.promote_joins(to_promote, True)
+ self.promote_joins(to_promote)
# Now relabel a copy of the rhs where-clause and add it to the current
# one.
@@ -658,7 +659,7 @@ class Query(object):
""" Decreases the reference count for this alias. """
self.alias_refcount[alias] -= amount
- def promote_joins(self, aliases, unconditional=False):
+ def promote_joins(self, aliases):
"""
Promotes recursively the join type of given aliases and its children to
an outer join. If 'unconditional' is False, the join is only promoted if
@@ -681,12 +682,14 @@ class Query(object):
# isn't really joined at all in the query, so we should not
# alter its join type.
continue
+ # Only the first alias (skipped above) should have None join_type
+ assert self.alias_map[alias].join_type is not None
parent_alias = self.alias_map[alias].lhs_alias
parent_louter = (parent_alias
and self.alias_map[parent_alias].join_type == self.LOUTER)
already_louter = self.alias_map[alias].join_type == self.LOUTER
- if ((unconditional or self.alias_map[alias].nullable
- or parent_louter) and not already_louter):
+ if ((self.alias_map[alias].nullable or parent_louter) and
+ not already_louter):
data = self.alias_map[alias]._replace(join_type=self.LOUTER)
self.alias_map[alias] = data
# Join type of 'alias' changed, so re-examine all aliases that
@@ -740,7 +743,7 @@ class Query(object):
self.group_by = [relabel_column(col) for col in self.group_by]
self.select = [SelectInfo(relabel_column(s.col), s.field)
for s in self.select]
- self.aggregates = SortedDict(
+ self.aggregates = OrderedDict(
(key, relabel_column(col)) for key, col in self.aggregates.items())
# 2. Rename the alias in the internal table/alias datastructures.
@@ -794,7 +797,7 @@ class Query(object):
assert current < ord('Z')
prefix = chr(current + 1)
self.alias_prefix = prefix
- change_map = SortedDict()
+ change_map = OrderedDict()
for pos, alias in enumerate(self.tables):
if alias in exceptions:
continue
@@ -985,7 +988,7 @@ class Query(object):
# If the aggregate references a model or field that requires a join,
# those joins must be LEFT OUTER - empty join rows must be returned
# in order for zeros to be returned for those aggregates.
- self.promote_joins(join_list, True)
+ self.promote_joins(join_list)
col = targets[0].column
source = sources[0]
@@ -996,6 +999,8 @@ class Query(object):
field_name = field_list[0]
source = opts.get_field(field_name)
col = field_name
+ # We want to have the alias in SELECT clause even if mask is set.
+ self.append_aggregate_mask([alias])
# Add the aggregate to the query
aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary)
@@ -1091,8 +1096,7 @@ class Query(object):
try:
field, sources, opts, join_list, path = self.setup_joins(
- parts, opts, alias, can_reuse, allow_many,
- allow_explicit_fk=True)
+ parts, opts, alias, can_reuse, allow_many,)
if can_reuse is not None:
can_reuse.update(join_list)
except MultiJoin as e:
@@ -1237,15 +1241,14 @@ class Query(object):
len(q_object.children))
return target_clause
- def names_to_path(self, names, opts, allow_many, allow_explicit_fk):
+ def names_to_path(self, names, opts, allow_many):
"""
Walks the names path and turns them PathInfo tuples. Note that a
single name in 'names' can generate multiple PathInfos (m2m for
example).
'names' is the path of names to travle, 'opts' is the model Options we
- start the name resolving from, 'allow_many' and 'allow_explicit_fk'
- are as for setup_joins().
+ start the name resolving from, 'allow_many' is as for setup_joins().
Returns a list of PathInfo tuples. In addition returns the final field
(the last used join field), and target (which is a field guaranteed to
@@ -1258,17 +1261,9 @@ class Query(object):
try:
field, model, direct, m2m = opts.get_field_by_name(name)
except FieldDoesNotExist:
- for f in opts.fields:
- if allow_explicit_fk and name == f.attname:
- # XXX: A hack to allow foo_id to work in values() for
- # backwards compatibility purposes. If we dropped that
- # feature, this could be removed.
- field, model, direct, m2m = opts.get_field_by_name(f.name)
- break
- else:
- available = opts.get_all_field_names() + list(self.aggregate_select)
- raise FieldError("Cannot resolve keyword %r into field. "
- "Choices are: %s" % (name, ", ".join(available)))
+ available = opts.get_all_field_names() + list(self.aggregate_select)
+ raise FieldError("Cannot resolve keyword %r into field. "
+ "Choices are: %s" % (name, ", ".join(available)))
# Check if we need any joins for concrete inheritance cases (the
# field lives in parent, but we are currently in one of its
# children)
@@ -1314,7 +1309,7 @@ class Query(object):
return path, final_field, targets
def setup_joins(self, names, opts, alias, can_reuse=None, allow_many=True,
- allow_explicit_fk=False, outer_if_first=False):
+ outer_if_first=False):
"""
Compute the necessary table joins for the passage through the fields
given in 'names'. 'opts' is the Options class for the current model
@@ -1329,9 +1324,6 @@ class Query(object):
If 'allow_many' is False, then any reverse foreign key seen will
generate a MultiJoin exception.
- The 'allow_explicit_fk' controls if field.attname is allowed in the
- lookups.
-
Returns the final field involved in the joins, the target field (used
for any 'where' constraint), the final 'opts' value, the joins and the
field path travelled to generate the joins.
@@ -1345,7 +1337,7 @@ class Query(object):
joins = [alias]
# First, generate the path for the names
path, final_field, targets = self.names_to_path(
- names, opts, allow_many, allow_explicit_fk)
+ names, opts, allow_many)
# Then, add the path to the query's joins. Note that we can't trim
# joins at this stage - we will need the information about join type
# of the trimmed joins.
@@ -1648,7 +1640,7 @@ class Query(object):
# dictionary with their parameters in 'select_params' so that
# subsequent updates to the select dictionary also adjust the
# parameters appropriately.
- select_pairs = SortedDict()
+ select_pairs = OrderedDict()
if select_params:
param_iter = iter(select_params)
else:
@@ -1661,7 +1653,7 @@ class Query(object):
entry_params.append(next(param_iter))
pos = entry.find("%s", pos + 2)
select_pairs[name] = (entry, entry_params)
- # This is order preserving, since self.extra_select is a SortedDict.
+ # This is order preserving, since self.extra_select is an OrderedDict.
self.extra.update(select_pairs)
if where or params:
self.where.add(ExtraWhere(where, params), AND)
@@ -1753,6 +1745,10 @@ class Query(object):
self.aggregate_select_mask = set(names)
self._aggregate_select_cache = None
+ def append_aggregate_mask(self, names):
+ if self.aggregate_select_mask is not None:
+ self.set_aggregate_mask(set(names).union(self.aggregate_select_mask))
+
def set_extra_mask(self, names):
"""
Set the mask of extra select items that will be returned by SELECT,
@@ -1766,7 +1762,7 @@ class Query(object):
self._extra_select_cache = None
def _aggregate_select(self):
- """The SortedDict of aggregate columns that are not masked, and should
+ """The OrderedDict of aggregate columns that are not masked, and should
be used in the SELECT clause.
This result is cached for optimization purposes.
@@ -1774,7 +1770,7 @@ class Query(object):
if self._aggregate_select_cache is not None:
return self._aggregate_select_cache
elif self.aggregate_select_mask is not None:
- self._aggregate_select_cache = SortedDict([
+ self._aggregate_select_cache = OrderedDict([
(k, v) for k, v in self.aggregates.items()
if k in self.aggregate_select_mask
])
@@ -1787,7 +1783,7 @@ class Query(object):
if self._extra_select_cache is not None:
return self._extra_select_cache
elif self.extra_select_mask is not None:
- self._extra_select_cache = SortedDict([
+ self._extra_select_cache = OrderedDict([
(k, v) for k, v in self.extra.items()
if k in self.extra_select_mask
])
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index 6aab02bd9a..8beb3fa74a 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -11,8 +11,6 @@ from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, SelectInfo
from django.db.models.sql.datastructures import Date, DateTime
from django.db.models.sql.query import Query
from django.db.models.sql.where import AND, Constraint
-from django.utils.functional import Promise
-from django.utils.encoding import force_text
from django.utils import six
from django.utils import timezone
@@ -147,10 +145,6 @@ class UpdateQuery(Query):
Used by add_update_values() as well as the "fast" update path when
saving models.
"""
- # Check that no Promise object passes to the query. Refs #10498.
- values_seq = [(value[0], value[1], force_text(value[2]))
- if isinstance(value[2], Promise) else value
- for value in values_seq]
self.values.extend(values_seq)
def add_related_update(self, model, field, value):
@@ -210,12 +204,6 @@ class InsertQuery(Query):
into the query, for example.
"""
self.fields = fields
- # Check that no Promise object reaches the DB. Refs #10498.
- for field in fields:
- for obj in objs:
- value = getattr(obj, field.attname)
- if isinstance(value, Promise):
- setattr(obj, field.attname, force_text(value))
self.objs = objs
self.raw = raw
diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py
index 2e83ecdce4..7b71580370 100644
--- a/django/db/models/sql/where.py
+++ b/django/db/models/sql/where.py
@@ -2,8 +2,6 @@
Code to manage the creation and SQL rendering of 'where' constraints.
"""
-from __future__ import absolute_import
-
import collections
import datetime
from itertools import repeat
diff --git a/django/db/utils.py b/django/db/utils.py
index c1bce7326b..bcfb06f584 100644
--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -1,4 +1,5 @@
from functools import wraps
+from importlib import import_module
import os
import pkgutil
from threading import local
@@ -7,7 +8,6 @@ import warnings
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import cached_property
-from django.utils.importlib import import_module
from django.utils.module_loading import import_by_path
from django.utils._os import upath
from django.utils import six
@@ -104,7 +104,7 @@ class DatabaseErrorWrapper(object):
def load_backend(backend_name):
# Look for a fully qualified database backend name
try:
- return import_module('.base', backend_name)
+ return import_module('%s.base' % backend_name)
except ImportError as e_user:
# The database backend wasn't found. Display a helpful error message
# listing all possible (built-in) database backends.
diff --git a/django/forms/__init__.py b/django/forms/__init__.py
index 2588098330..34896d948d 100644
--- a/django/forms/__init__.py
+++ b/django/forms/__init__.py
@@ -2,8 +2,6 @@
Django validation and HTML form handling.
"""
-from __future__ import absolute_import
-
from django.core.exceptions import ValidationError
from django.forms.fields import *
from django.forms.forms import *
diff --git a/django/forms/extras/__init__.py b/django/forms/extras/__init__.py
index d801e4fa80..28316f472c 100644
--- a/django/forms/extras/__init__.py
+++ b/django/forms/extras/__init__.py
@@ -1,3 +1 @@
-from __future__ import absolute_import
-
from django.forms.extras.widgets import *
diff --git a/django/forms/fields.py b/django/forms/fields.py
index a794c02e9f..e995187682 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -2,7 +2,7 @@
Field classes.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import copy
import datetime
@@ -118,6 +118,8 @@ class Field(object):
super(Field, self).__init__()
def prepare_value(self, value):
+ if self.widget.is_localized:
+ value = formats.localize_input(value)
return value
def to_python(self, value):
@@ -460,6 +462,7 @@ class DateTimeField(BaseTemporalField):
}
def prepare_value(self, value):
+ value = super(DateTimeField, self).prepare_value(value)
if isinstance(value, datetime.datetime):
value = to_current_timezone(value)
return value
@@ -952,15 +955,20 @@ class MultiValueField(Field):
"""
default_error_messages = {
'invalid': _('Enter a list of values.'),
+ 'incomplete': _('Enter a complete value.'),
}
def __init__(self, fields=(), *args, **kwargs):
+ self.require_all_fields = kwargs.pop('require_all_fields', True)
super(MultiValueField, self).__init__(*args, **kwargs)
- # Set 'required' to False on the individual fields, because the
- # required validation will be handled by MultiValueField, not by those
- # individual fields.
for f in fields:
- f.required = False
+ f.error_messages.setdefault('incomplete',
+ self.error_messages['incomplete'])
+ if self.require_all_fields:
+ # Set 'required' to False on the individual fields, because the
+ # required validation will be handled by MultiValueField, not
+ # by those individual fields.
+ f.required = False
self.fields = fields
def validate(self, value):
@@ -990,15 +998,26 @@ class MultiValueField(Field):
field_value = value[i]
except IndexError:
field_value = None
- if self.required and field_value in self.empty_values:
- raise ValidationError(self.error_messages['required'], code='required')
+ if field_value in self.empty_values:
+ if self.require_all_fields:
+ # Raise a 'required' error if the MultiValueField is
+ # required and any field is empty.
+ if self.required:
+ raise ValidationError(self.error_messages['required'], code='required')
+ elif field.required:
+ # Otherwise, add an 'incomplete' error to the list of
+ # collected errors and skip field cleaning, if a required
+ # field is empty.
+ if field.error_messages['incomplete'] not in errors:
+ errors.append(field.error_messages['incomplete'])
+ continue
try:
clean_data.append(field.clean(field_value))
except ValidationError as e:
# Collect all validation errors in a single list, which we'll
# raise at the end of clean(), rather than raising a single
- # exception for the first error we encounter.
- errors.extend(e.error_list)
+ # exception for the first error we encounter. Skip duplicates.
+ errors.extend(m for m in e.error_list if m not in errors)
if errors:
raise ValidationError(errors)
diff --git a/django/forms/forms.py b/django/forms/forms.py
index e144eb60f8..c2b700ce77 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -2,8 +2,9 @@
Form classes
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
+from collections import OrderedDict
import copy
import warnings
@@ -11,7 +12,6 @@ from django.core.exceptions import ValidationError
from django.forms.fields import Field, FileField
from django.forms.util import flatatt, ErrorDict, ErrorList
from django.forms.widgets import Media, media_property, TextInput, Textarea
-from django.utils.datastructures import SortedDict
from django.utils.html import conditional_escape, format_html
from django.utils.encoding import smart_text, force_text, python_2_unicode_compatible
from django.utils.safestring import mark_safe
@@ -55,7 +55,7 @@ def get_declared_fields(bases, attrs, with_base_fields=True):
if hasattr(base, 'declared_fields'):
fields = list(six.iteritems(base.declared_fields)) + fields
- return SortedDict(fields)
+ return OrderedDict(fields)
class DeclarativeFieldsMetaclass(type):
"""
@@ -297,9 +297,12 @@ class BaseForm(object):
def _clean_form(self):
try:
- self.cleaned_data = self.clean()
+ cleaned_data = self.clean()
except ValidationError as e:
self._errors[NON_FIELD_ERRORS] = self.error_class(e.messages)
+ else:
+ if cleaned_data is not None:
+ self.cleaned_data = cleaned_data
def _post_clean(self):
"""
@@ -509,20 +512,23 @@ class BoundField(object):
)
return self.field.prepare_value(data)
- def label_tag(self, contents=None, attrs=None):
+ def label_tag(self, contents=None, attrs=None, label_suffix=None):
"""
Wraps the given contents in a <label>, if the field has an ID attribute.
contents should be 'mark_safe'd to avoid HTML escaping. If contents
aren't given, uses the field's HTML-escaped label.
If attrs are given, they're used as HTML attributes on the <label> tag.
+
+ label_suffix allows overriding the form's label_suffix.
"""
contents = contents or self.label
# Only add the suffix if the label does not end in punctuation.
# Translators: If found as last label character, these punctuation
# characters will prevent the default label_suffix to be appended to the label
- if self.form.label_suffix and contents and contents[-1] not in _(':?.!'):
- contents = format_html('{0}{1}', contents, self.form.label_suffix)
+ label_suffix = label_suffix if label_suffix is not None else self.form.label_suffix
+ if label_suffix and contents and contents[-1] not in _(':?.!'):
+ contents = format_html('{0}{1}', contents, label_suffix)
widget = self.field.widget
id_ = widget.attrs.get('id') or self.auto_id
if id_:
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index cb3126e6d7..8a379ff3cf 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
from django.core.exceptions import ValidationError
from django.forms import Form
diff --git a/django/forms/models.py b/django/forms/models.py
index 1d3bec1635..a5b82e521d 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -3,8 +3,9 @@ Helper functions for creating Form classes from Django models
and database field objects.
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
+from collections import OrderedDict
import warnings
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
@@ -15,7 +16,6 @@ from django.forms.util import ErrorList
from django.forms.widgets import (SelectMultiple, HiddenInput,
MultipleHiddenInput, media_property, CheckboxSelectMultiple)
from django.utils.encoding import smart_text, force_text
-from django.utils.datastructures import SortedDict
from django.utils import six
from django.utils.text import get_text_list, capfirst
from django.utils.translation import ugettext_lazy as _, ugettext, string_concat
@@ -142,7 +142,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
formfield_callback=None, localized_fields=None,
labels=None, help_texts=None, error_messages=None):
"""
- Returns a ``SortedDict`` containing form fields for the given model.
+ Returns a ``OrderedDict`` containing form fields for the given model.
``fields`` is an optional list of field names. If provided, only the named
fields will be included in the returned fields.
@@ -199,9 +199,9 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
field_list.append((f.name, formfield))
else:
ignored.append(f.name)
- field_dict = SortedDict(field_list)
+ field_dict = OrderedDict(field_list)
if fields:
- field_dict = SortedDict(
+ field_dict = OrderedDict(
[(f, field_dict.get(f)) for f in fields
if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored)]
)
@@ -717,7 +717,8 @@ class BaseModelFormSet(BaseFormSet):
obj = self._existing_object(pk_value)
if form in forms_to_delete:
self.deleted_objects.append(obj)
- obj.delete()
+ if commit:
+ obj.delete()
continue
if form.has_changed():
self.changed_objects.append((obj, form.changed_data))
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index a92e5a56ce..0a5059a9c2 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -2,7 +2,7 @@
HTML Widget classes
"""
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import copy
from itertools import chain
@@ -511,6 +511,8 @@ class Select(Widget):
return mark_safe('\n'.join(output))
def render_option(self, selected_choices, option_value, option_label):
+ if option_value == None:
+ option_value = ''
option_value = force_text(option_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
@@ -838,6 +840,11 @@ class MultiWidget(Widget):
obj.widgets = copy.deepcopy(self.widgets)
return obj
+ @property
+ def needs_multipart_form(self):
+ return any(w.needs_multipart_form for w in self.widgets)
+
+
class SplitDateTimeWidget(MultiWidget):
"""
A Widget that splits datetime input into two <input type="text"> boxes.
diff --git a/django/http/cookie.py b/django/http/cookie.py
index 50ff549caf..62d5c61a4d 100644
--- a/django/http/cookie.py
+++ b/django/http/cookie.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
from django.utils.encoding import force_str
from django.utils import six
diff --git a/django/http/request.py b/django/http/request.py
index b7f9d241a7..9f9f32b1b4 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import copy
import os
@@ -68,14 +68,19 @@ class HttpRequest(object):
if server_port != ('443' if self.is_secure() else '80'):
host = '%s:%s' % (host, server_port)
- allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS
+ # There is no hostname validation when DEBUG=True
+ if settings.DEBUG:
+ return host
+
domain, port = split_domain_port(host)
- if domain and validate_host(domain, allowed_hosts):
+ if domain and validate_host(domain, settings.ALLOWED_HOSTS):
return host
else:
msg = "Invalid HTTP_HOST header: %r." % host
if domain:
msg += "You may need to add %r to ALLOWED_HOSTS." % domain
+ else:
+ msg += "The domain name provided is not valid according to RFC 1034/1035"
raise DisallowedHost(msg)
def get_full_path(self):
diff --git a/django/http/response.py b/django/http/response.py
index 1f0849c0bf..aaebefe022 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import datetime
import time
@@ -14,7 +14,7 @@ from django.core import signing
from django.core.exceptions import DisallowedRedirect
from django.http.cookie import SimpleCookie
from django.utils import six, timezone
-from django.utils.encoding import force_bytes, iri_to_uri
+from django.utils.encoding import force_bytes, force_text, iri_to_uri
from django.utils.http import cookie_date
from django.utils.six.moves import map
@@ -393,7 +393,7 @@ class HttpResponseRedirectBase(HttpResponse):
allowed_schemes = ['http', 'https', 'ftp']
def __init__(self, redirect_to, *args, **kwargs):
- parsed = urlparse(redirect_to)
+ parsed = urlparse(force_text(redirect_to))
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme)
super(HttpResponseRedirectBase, self).__init__(*args, **kwargs)
diff --git a/django/middleware/locale.py b/django/middleware/locale.py
index 4e0a4753ce..cd5d2cce71 100644
--- a/django/middleware/locale.py
+++ b/django/middleware/locale.py
@@ -1,12 +1,13 @@
"This is the locale selecting middleware that will look at accept headers"
+from collections import OrderedDict
+
from django.conf import settings
from django.core.urlresolvers import (is_valid_path, get_resolver,
LocaleRegexURLResolver)
from django.http import HttpResponseRedirect
from django.utils.cache import patch_vary_headers
from django.utils import translation
-from django.utils.datastructures import SortedDict
class LocaleMiddleware(object):
@@ -19,7 +20,7 @@ class LocaleMiddleware(object):
"""
def __init__(self):
- self._supported_languages = SortedDict(settings.LANGUAGES)
+ self._supported_languages = OrderedDict(settings.LANGUAGES)
self._is_language_prefix_patterns_used = False
for url_pattern in get_resolver(None).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
diff --git a/django/template/base.py b/django/template/base.py
index 364b428070..ed4196012a 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -1,13 +1,13 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import re
from functools import partial
+from importlib import import_module
from inspect import getargspec
from django.conf import settings
from django.template.context import (Context, RequestContext,
ContextPopException)
-from django.utils.importlib import import_module
from django.utils.itercompat import is_iterable
from django.utils.text import (smart_split, unescape_string_literal,
get_text_list)
diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py
index cea01558d9..b25f14aace 100644
--- a/django/template/loaders/app_directories.py
+++ b/django/template/loaders/app_directories.py
@@ -3,6 +3,7 @@ Wrapper for loading templates from "templates" directories in INSTALLED_APPS
packages.
"""
+from importlib import import_module
import os
import sys
@@ -11,7 +12,6 @@ from django.core.exceptions import ImproperlyConfigured
from django.template.base import TemplateDoesNotExist
from django.template.loader import BaseLoader
from django.utils._os import safe_join
-from django.utils.importlib import import_module
from django.utils import six
# At compile time, cache the directories to search.
diff --git a/django/template/loaders/cached.py b/django/template/loaders/cached.py
index c61045de06..b33b4e8446 100644
--- a/django/template/loaders/cached.py
+++ b/django/template/loaders/cached.py
@@ -13,6 +13,7 @@ class Loader(BaseLoader):
def __init__(self, loaders):
self.template_cache = {}
+ self.find_template_cache = {}
self._loaders = loaders
self._cached_loaders = []
@@ -28,21 +29,34 @@ class Loader(BaseLoader):
self._cached_loaders = cached_loaders
return self._cached_loaders
- def find_template(self, name, dirs=None):
- for loader in self.loaders:
- try:
- template, display_name = loader(name, dirs)
- return (template, make_origin(display_name, loader, name, dirs))
- except TemplateDoesNotExist:
- pass
- raise TemplateDoesNotExist(name)
-
- def load_template(self, template_name, template_dirs=None):
- key = template_name
+ def cache_key(self, template_name, template_dirs):
if template_dirs:
# If template directories were specified, use a hash to differentiate
- key = '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
+ return '-'.join([template_name, hashlib.sha1(force_bytes('|'.join(template_dirs))).hexdigest()])
+ else:
+ return template_name
+
+ def find_template(self, name, dirs=None):
+ key = self.cache_key(name, dirs)
+ try:
+ result = self.find_template_cache[key]
+ except KeyError:
+ result = None
+ for loader in self.loaders:
+ try:
+ template, display_name = loader(name, dirs)
+ except TemplateDoesNotExist:
+ pass
+ else:
+ result = (template, make_origin(display_name, loader, name, dirs))
+ self.find_template_cache[key] = result
+ if result:
+ return result
+ else:
+ raise TemplateDoesNotExist(name)
+ def load_template(self, template_name, template_dirs=None):
+ key = self.cache_key(template_name, template_dirs)
try:
template = self.template_cache[key]
except KeyError:
@@ -62,3 +76,4 @@ class Loader(BaseLoader):
def reset(self):
"Empty the template cache."
self.template_cache.clear()
+ self.find_template_cache.clear()
diff --git a/django/test/client.py b/django/test/client.py
index 8dbd33ccf1..754d4d73f8 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -5,6 +5,7 @@ import os
import re
import mimetypes
from copy import copy
+from importlib import import_module
from io import BytesIO
try:
from urllib.parse import unquote, urlparse, urlsplit
@@ -25,7 +26,6 @@ from django.test import signals
from django.utils.functional import curry
from django.utils.encoding import force_bytes, force_str
from django.utils.http import urlencode
-from django.utils.importlib import import_module
from django.utils.itercompat import is_iterable
from django.utils import six
from django.test.utils import ContextList
diff --git a/django/test/simple.py b/django/test/simple.py
index 019eff2421..b2ab1269b5 100644
--- a/django/test/simple.py
+++ b/django/test/simple.py
@@ -3,6 +3,7 @@ This module is pending deprecation as of Django 1.6 and will be removed in
version 1.8.
"""
+from importlib import import_module
import json
import re
import unittest as real_unittest
@@ -15,7 +16,6 @@ from django.test.utils import compare_xml, strip_quotes
# django.utils.unittest is deprecated, but so is django.test.simple,
# and the latter will be removed before the former.
from django.utils import unittest
-from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
__all__ = ('DjangoTestSuiteRunner',)
diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py
index 20b771e95b..706c09593b 100644
--- a/django/utils/datastructures.py
+++ b/django/utils/datastructures.py
@@ -1,7 +1,7 @@
import copy
+import warnings
from django.utils import six
-
class MergeDict(object):
"""
A simple class for creating new "virtual" dictionaries that actually look
@@ -124,6 +124,10 @@ class SortedDict(dict):
return instance
def __init__(self, data=None):
+ warnings.warn(
+ "SortedDict is deprecated and will be removed in Django 1.9.",
+ PendingDeprecationWarning, stacklevel=2
+ )
if data is None or isinstance(data, dict):
data = data or []
super(SortedDict, self).__init__(data)
diff --git a/django/utils/formats.py b/django/utils/formats.py
index cc66149026..6b89b40ecd 100644
--- a/django/utils/formats.py
+++ b/django/utils/formats.py
@@ -1,10 +1,10 @@
import decimal
import datetime
+from importlib import import_module
import unicodedata
from django.conf import settings
from django.utils import dateformat, numberformat, datetime_safe
-from django.utils.importlib import import_module
from django.utils.encoding import force_str
from django.utils.functional import lazy
from django.utils.safestring import mark_safe
@@ -54,7 +54,7 @@ def iter_format_modules(lang):
for location in format_locations:
for loc in locales:
try:
- yield import_module('.formats', location % loc)
+ yield import_module('%s.formats' % (location % loc))
except ImportError:
pass
diff --git a/django/utils/html.py b/django/utils/html.py
index 4893b6b18a..89e790d96f 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -4,13 +4,13 @@ from __future__ import unicode_literals
import re
try:
- from urllib.parse import quote, urlsplit, urlunsplit
+ from urllib.parse import quote, unquote, urlsplit, urlunsplit
except ImportError: # Python 2
- from urllib import quote
+ from urllib import quote, unquote
from urlparse import urlsplit, urlunsplit
from django.utils.safestring import SafeData, mark_safe
-from django.utils.encoding import force_bytes, force_text
+from django.utils.encoding import force_text, force_str
from django.utils.functional import allow_lazy
from django.utils import six
from django.utils.text import normalize_newlines
@@ -26,7 +26,6 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('&lt;', '&gt;')]
DOTS = ['&middot;', '*', '\u2022', '&#149;', '&bull;', '&#8226;']
unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)')
-unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
word_split_re = re.compile(r'(\s+)')
simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE)
simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE)
@@ -185,11 +184,9 @@ def smart_urlquote(url):
# invalid IPv6 URL (normally square brackets in hostname part).
pass
- # An URL is considered unquoted if it contains no % characters or
- # contains a % not followed by two hexadecimal digits. See #9655.
- if '%' not in url or unquoted_percents_re.search(url):
- # See http://bugs.python.org/issue2637
- url = quote(force_bytes(url), safe=b'!*\'();:@&=+$,/?#[]~')
+ url = unquote(force_str(url))
+ # See http://bugs.python.org/issue2637
+ url = quote(url, safe=b'!*\'();:@&=+$,/?#[]~')
return force_text(url)
diff --git a/django/utils/importlib.py b/django/utils/importlib.py
index 703ba7f6d8..26e45589e3 100644
--- a/django/utils/importlib.py
+++ b/django/utils/importlib.py
@@ -1,6 +1,11 @@
# Taken from Python 2.7 with permission from/by the original author.
+import warnings
import sys
+warnings.warn("django.utils.importlib will be removed in Django 1.9.",
+ PendingDeprecationWarning, stacklevel=2)
+
+
def _resolve_name(name, package, level):
"""Return the absolute name of the module to be imported."""
if not hasattr(package, 'rindex'):
diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py
index ede585e562..359982e6ba 100644
--- a/django/utils/module_loading.py
+++ b/django/utils/module_loading.py
@@ -1,10 +1,10 @@
import imp
+from importlib import import_module
import os
import sys
from django.core.exceptions import ImproperlyConfigured
from django.utils import six
-from django.utils.importlib import import_module
def import_by_path(dotted_path, error_prefix=''):
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 195badfc00..11463d8f9b 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -1,16 +1,16 @@
"""Translation helper functions."""
from __future__ import unicode_literals
+from collections import OrderedDict
import locale
import os
import re
import sys
import gettext as gettext_module
+from importlib import import_module
from threading import local
import warnings
-from django.utils.importlib import import_module
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_str, force_text
from django.utils.functional import memoize
from django.utils._os import upath
@@ -369,7 +369,7 @@ def get_supported_language_variant(lang_code, supported=None, strict=False):
"""
if supported is None:
from django.conf import settings
- supported = SortedDict(settings.LANGUAGES)
+ supported = OrderedDict(settings.LANGUAGES)
if lang_code:
# if fr-CA is not supported, try fr-ca; if that fails, fallback to fr.
generic_lang_code = lang_code.split('-')[0]
@@ -396,7 +396,7 @@ def get_language_from_path(path, supported=None, strict=False):
"""
if supported is None:
from django.conf import settings
- supported = SortedDict(settings.LANGUAGES)
+ supported = OrderedDict(settings.LANGUAGES)
regex_match = language_code_prefix_re.match(path)
if not regex_match:
return None
@@ -418,7 +418,7 @@ def get_language_from_request(request, check_path=False):
"""
global _accepted
from django.conf import settings
- supported = SortedDict(settings.LANGUAGES)
+ supported = OrderedDict(settings.LANGUAGES)
if check_path:
lang_code = get_language_from_path(request.path_info, supported)
diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py
index 78ae6b1442..a611981e79 100644
--- a/django/views/decorators/debug.py
+++ b/django/views/decorators/debug.py
@@ -1,5 +1,7 @@
import functools
+from django.http import HttpRequest
+
def sensitive_variables(*variables):
"""
@@ -62,6 +64,10 @@ def sensitive_post_parameters(*parameters):
def decorator(view):
@functools.wraps(view)
def sensitive_post_parameters_wrapper(request, *args, **kwargs):
+ assert isinstance(request, HttpRequest), (
+ "sensitive_post_parameters didn't receive an HttpRequest. If you "
+ "are decorating a classmethod, be sure to use @method_decorator."
+ )
if parameters:
request.sensitive_post_parameters = parameters
else:
diff --git a/django/views/defaults.py b/django/views/defaults.py
index e9d0f16a33..2f54fcd263 100644
--- a/django/views/defaults.py
+++ b/django/views/defaults.py
@@ -21,11 +21,14 @@ def page_not_found(request, template_name='404.html'):
"""
try:
template = loader.get_template(template_name)
+ content_type = None # Django will use DEFAULT_CONTENT_TYPE
except TemplateDoesNotExist:
template = Template(
'<h1>Not Found</h1>'
'<p>The requested URL {{ request_path }} was not found on this server.</p>')
- return http.HttpResponseNotFound(template.render(RequestContext(request, {'request_path': request.path})))
+ content_type = 'text/html'
+ body = template.render(RequestContext(request, {'request_path': request.path}))
+ return http.HttpResponseNotFound(body, content_type=content_type)
@requires_csrf_token
@@ -39,7 +42,7 @@ def server_error(request, template_name='500.html'):
try:
template = loader.get_template(template_name)
except TemplateDoesNotExist:
- return http.HttpResponseServerError('<h1>Server Error (500)</h1>')
+ return http.HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
return http.HttpResponseServerError(template.render(Context({})))
@@ -54,7 +57,7 @@ def bad_request(request, template_name='400.html'):
try:
template = loader.get_template(template_name)
except TemplateDoesNotExist:
- return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>')
+ return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>', content_type='text/html')
return http.HttpResponseBadRequest(template.render(Context({})))
@@ -75,7 +78,7 @@ def permission_denied(request, template_name='403.html'):
try:
template = loader.get_template(template_name)
except TemplateDoesNotExist:
- return http.HttpResponseForbidden('<h1>403 Forbidden</h1>')
+ return http.HttpResponseForbidden('<h1>403 Forbidden</h1>', content_type='text/html')
return http.HttpResponseForbidden(template.render(RequestContext(request)))
diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py
index 23000641b4..5ce8092c67 100644
--- a/django/views/generic/detail.py
+++ b/django/views/generic/detail.py
@@ -57,19 +57,23 @@ class SingleObjectMixin(ContextMixin):
def get_queryset(self):
"""
- Get the queryset to look an object up against. May not be called if
- `get_object` is overridden.
+ Return the `QuerySet` that will be used to look up the object.
+
+ Note that this method is called by the default implementation of
+ `get_object` and may not be called if `get_object` is overriden.
"""
if self.queryset is None:
if self.model:
return self.model._default_manager.all()
else:
- raise ImproperlyConfigured("%(cls)s is missing a queryset. Define "
- "%(cls)s.model, %(cls)s.queryset, or override "
- "%(cls)s.get_queryset()." % {
- 'cls': self.__class__.__name__
- })
- return self.queryset._clone()
+ raise ImproperlyConfigured(
+ "%(cls)s is missing a QuerySet. Define "
+ "%(cls)s.model, %(cls)s.queryset, or override "
+ "%(cls)s.get_queryset()." % {
+ 'cls': self.__class__.__name__
+ }
+ )
+ return self.queryset.all()
def get_slug_field(self):
"""
diff --git a/django/views/generic/list.py b/django/views/generic/list.py
index 1aff3454f4..7381fd1be9 100644
--- a/django/views/generic/list.py
+++ b/django/views/generic/list.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django.core.paginator import Paginator, InvalidPage
from django.core.exceptions import ImproperlyConfigured
+from django.db.models.query import QuerySet
from django.http import Http404
from django.utils.translation import ugettext as _
from django.views.generic.base import TemplateResponseMixin, ContextMixin, View
@@ -22,18 +23,25 @@ class MultipleObjectMixin(ContextMixin):
def get_queryset(self):
"""
- Get the list of items for this view. This must be an iterable, and may
- be a queryset (in which qs-specific behavior will be enabled).
+ Return the list of items for this view.
+
+ The return value must be an iterable and may be an instance of
+ `QuerySet` in which case `QuerySet` specific behavior will be enabled.
"""
if self.queryset is not None:
queryset = self.queryset
- if hasattr(queryset, '_clone'):
- queryset = queryset._clone()
+ if isinstance(queryset, QuerySet):
+ queryset = queryset.all()
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
- raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'"
- % self.__class__.__name__)
+ raise ImproperlyConfigured(
+ "%(cls)s is missing a QuerySet. Define "
+ "%(cls)s.model, %(cls)s.queryset, or override "
+ "%(cls)s.get_queryset()." % {
+ 'cls': self.__class__.__name__
+ }
+ )
return queryset
def paginate_queryset(self, queryset, page_size):
diff --git a/django/views/i18n.py b/django/views/i18n.py
index 71ac005855..c913922999 100644
--- a/django/views/i18n.py
+++ b/django/views/i18n.py
@@ -1,3 +1,4 @@
+import importlib
import json
import os
import gettext as gettext_module
@@ -5,7 +6,6 @@ import gettext as gettext_module
from django import http
from django.conf import settings
from django.template import Context, Template
-from django.utils import importlib
from django.utils.translation import check_for_language, to_locale, get_language
from django.utils.encoding import smart_text
from django.utils.formats import get_format_modules, get_format