1
0
mirror of https://github.com/django/django.git synced 2025-06-03 18:49:12 +00:00

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
This commit is contained in:
Andrew Godwin 2013-08-09 14:17:30 +01:00
commit de64c4d6e9
489 changed files with 3840 additions and 1593 deletions

View File

@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
Paul Collier <paul@paul-collier.com> Paul Collier <paul@paul-collier.com>
Paul Collins <paul.collins.iii@gmail.com> Paul Collins <paul.collins.iii@gmail.com>
Robert Coup Robert Coup
Alex Couper <http://alexcouper.com/>
Deric Crago <deric.crago@gmail.com> Deric Crago <deric.crago@gmail.com>
Brian Fabian Crain <http://www.bfc.do/> Brian Fabian Crain <http://www.bfc.do/>
David Cramer <dcramer@gmail.com> David Cramer <dcramer@gmail.com>
@ -416,6 +417,7 @@ answer newbie questions, and generally made Django that much better:
Zain Memon Zain Memon
Christian Metts Christian Metts
michal@plovarna.cz michal@plovarna.cz
Justin Michalicek <jmichalicek@gmail.com>
Slawek Mikula <slawek dot mikula at gmail dot com> Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com> Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com> Shawn Milochik <shawn@milochik.com>
@ -542,6 +544,7 @@ answer newbie questions, and generally made Django that much better:
smurf@smurf.noris.de smurf@smurf.noris.de
Vsevolod Solovyov Vsevolod Solovyov
George Song <george@damacy.net> George Song <george@damacy.net>
Jimmy Song <jaejoon@gmail.com>
sopel sopel
Leo Soto <leo.soto@gmail.com> Leo Soto <leo.soto@gmail.com>
Thomas Sorrel Thomas Sorrel

View File

@ -6,6 +6,7 @@ variable, and then from django.conf.global_settings; see the global settings fil
a list of all possible variables. a list of all possible variables.
""" """
import importlib
import logging import logging
import os import os
import sys import sys
@ -15,7 +16,6 @@ import warnings
from django.conf import global_settings from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import LazyObject, empty from django.utils.functional import LazyObject, empty
from django.utils import importlib
from django.utils.module_loading import import_by_path from django.utils.module_loading import import_by_path
from django.utils import six from django.utils import six
@ -107,6 +107,9 @@ class BaseSettings(object):
elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types): elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types):
raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set " raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set "
"to a tuple, not a string.") "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) object.__setattr__(self, name, value)

View File

@ -1,7 +1,8 @@
from importlib import import_module
from django.core.urlresolvers import (RegexURLPattern, from django.core.urlresolvers import (RegexURLPattern,
RegexURLResolver, LocaleRegexURLResolver) RegexURLResolver, LocaleRegexURLResolver)
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
from django.utils import six from django.utils import six

View File

@ -17,8 +17,8 @@ def autodiscover():
""" """
import copy import copy
from importlib import import_module
from django.conf import settings from django.conf import settings
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule from django.utils.module_loading import module_has_submodule
for app in settings.INSTALLED_APPS: for app in settings.INSTALLED_APPS:

View File

@ -87,7 +87,7 @@ class SimpleListFilter(ListFilter):
def lookups(self, request, model_admin): 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 raise NotImplementedError

View File

@ -125,14 +125,16 @@ class AdminField(object):
contents = conditional_escape(force_text(self.field.label)) contents = conditional_escape(force_text(self.field.label))
if self.is_checkbox: if self.is_checkbox:
classes.append('vCheckboxLabel') classes.append('vCheckboxLabel')
else:
contents += ':'
if self.field.field.required: if self.field.field.required:
classes.append('required') classes.append('required')
if not self.is_first: if not self.is_first:
classes.append('inline') classes.append('inline')
attrs = {'class': ' '.join(classes)} if classes else {} 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): def errors(self):
return mark_safe(self.field.errors.as_ul()) return mark_safe(self.field.errors.as_ul())

View File

@ -4,7 +4,7 @@ from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.admin.util import quote 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.translation import ugettext, ugettext_lazy as _
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.encoding import python_2_unicode_compatible 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: if self.content_type and self.object_id:
url_name = 'admin:%s_%s_change' % (self.content_type.app_label, self.content_type.model) 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 return None

View File

@ -1,6 +1,8 @@
from collections import OrderedDict
import copy import copy
import operator import operator
from functools import partial, reduce, update_wrapper from functools import partial, reduce, update_wrapper
import warnings
from django import forms from django import forms
from django.conf import settings 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.shortcuts import get_object_or_404
from django.template.response import SimpleTemplateResponse, TemplateResponse from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.datastructures import SortedDict
from django.utils.html import escape, escapejs from django.utils.html import escape, escapejs
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils import six from django.utils import six
@ -69,6 +70,7 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = {
models.CharField: {'widget': widgets.AdminTextInputWidget}, models.CharField: {'widget': widgets.AdminTextInputWidget},
models.ImageField: {'widget': widgets.AdminFileWidget}, models.ImageField: {'widget': widgets.AdminFileWidget},
models.FileField: {'widget': widgets.AdminFileWidget}, models.FileField: {'widget': widgets.AdminFileWidget},
models.EmailField: {'widget': widgets.AdminEmailInputWidget},
} }
csrf_protect_m = method_decorator(csrf_protect) csrf_protect_m = method_decorator(csrf_protect)
@ -237,13 +239,49 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
return db_field.formfield(**kwargs) 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: if self.fieldsets:
return self.fieldsets return self.fieldsets
elif self.fields: elif self.fields:
return [(None, {'fields': self.fields})] return [(None, {'fields': self.fields})]
return None 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): def get_ordering(self, request):
""" """
@ -263,34 +301,6 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
""" """
return self.prepopulated_fields 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): def get_queryset(self, request):
""" """
Returns a QuerySet of all model instances that can be edited by the 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), 'delete': self.has_delete_permission(request),
} }
def get_fieldsets(self, request, obj=None): def get_fields(self, request, obj=None):
"Hook for specifying fieldsets for the add form." if self.fields:
if self.declared_fieldsets: return self.fields
return self.declared_fieldsets
form = self.get_form(request, obj, fields=None) form = self.get_form(request, obj, fields=None)
fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
return [(None, {'fields': fields})]
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
""" """
@ -670,7 +678,7 @@ class ModelAdmin(BaseModelAdmin):
# want *any* actions enabled on this page. # want *any* actions enabled on this page.
from django.contrib.admin.views.main import _is_changelist_popup from django.contrib.admin.views.main import _is_changelist_popup
if self.actions is None or _is_changelist_popup(request): if self.actions is None or _is_changelist_popup(request):
return SortedDict() return OrderedDict()
actions = [] actions = []
@ -691,8 +699,8 @@ class ModelAdmin(BaseModelAdmin):
# get_action might have returned None, so filter any of those out. # get_action might have returned None, so filter any of those out.
actions = filter(None, actions) actions = filter(None, actions)
# Convert the actions into a SortedDict keyed by name. # Convert the actions into an OrderedDict keyed by name.
actions = SortedDict([ actions = OrderedDict([
(name, (func, name, desc)) (name, (func, name, desc))
for func, name, desc in actions for func, name, desc in actions
]) ])
@ -766,11 +774,50 @@ class ModelAdmin(BaseModelAdmin):
""" """
return self.list_filter 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): def get_preserved_filters(self, request):
""" """
Returns the preserved filters querystring. Returns the preserved filters querystring.
""" """
match = request.resolver_match match = request.resolver_match
if self.preserve_filters and match: if self.preserve_filters and match:
opts = self.model._meta opts = self.model._meta
@ -1106,17 +1153,7 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form_validated = False form_validated = False
new_object = self.model() new_object = self.model()
prefixes = {} formsets = self._create_formsets(request, new_object, inline_instances)
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)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, False) self.save_model(request, new_object, form, False)
self.save_related(request, form, formsets, False) self.save_related(request, form, formsets, False)
@ -1134,15 +1171,7 @@ class ModelAdmin(BaseModelAdmin):
if isinstance(f, models.ManyToManyField): if isinstance(f, models.ManyToManyField):
initial[k] = initial[k].split(",") initial[k] = initial[k].split(",")
form = ModelForm(initial=initial) form = ModelForm(initial=initial)
prefixes = {} formsets = self._create_formsets(request, self.model(), inline_instances)
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)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
self.get_prepopulated_fields(request), self.get_prepopulated_fields(request),
@ -1194,7 +1223,6 @@ class ModelAdmin(BaseModelAdmin):
current_app=self.admin_site.name)) current_app=self.admin_site.name))
ModelForm = self.get_form(request, obj) ModelForm = self.get_form(request, obj)
formsets = []
inline_instances = self.get_inline_instances(request, obj) inline_instances = self.get_inline_instances(request, obj)
if request.method == 'POST': if request.method == 'POST':
form = ModelForm(request.POST, request.FILES, instance=obj) form = ModelForm(request.POST, request.FILES, instance=obj)
@ -1204,18 +1232,7 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form_validated = False form_validated = False
new_object = obj new_object = obj
prefixes = {} formsets = self._create_formsets(request, new_object, inline_instances)
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)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, True) self.save_model(request, new_object, form, True)
self.save_related(request, form, formsets, True) self.save_related(request, form, formsets, True)
@ -1225,15 +1242,7 @@ class ModelAdmin(BaseModelAdmin):
else: else:
form = ModelForm(instance=obj) form = ModelForm(instance=obj)
prefixes = {} formsets = self._create_formsets(request, obj, inline_instances)
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)
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
self.get_prepopulated_fields(request, obj), self.get_prepopulated_fields(request, obj),
@ -1280,6 +1289,7 @@ class ModelAdmin(BaseModelAdmin):
list_display = self.get_list_display(request) list_display = self.get_list_display(request)
list_display_links = self.get_list_display_links(request, list_display) list_display_links = self.get_list_display_links(request, list_display)
list_filter = self.get_list_filter(request) 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 # Check actions to see if any are available on this changelist
actions = self.get_actions(request) actions = self.get_actions(request)
@ -1291,9 +1301,9 @@ class ModelAdmin(BaseModelAdmin):
try: try:
cl = ChangeList(request, self.model, list_display, cl = ChangeList(request, self.model, list_display,
list_display_links, list_filter, self.date_hierarchy, list_display_links, list_filter, self.date_hierarchy,
self.search_fields, self.list_select_related, search_fields, self.list_select_related, self.list_per_page,
self.list_per_page, self.list_max_show_all, self.list_editable, self.list_max_show_all, self.list_editable, self)
self)
except IncorrectLookupParameters: except IncorrectLookupParameters:
# Wacky lookup parameters were given, so redirect to the main # Wacky lookup parameters were given, so redirect to the main
# changelist page, without parameters, and pass an 'invalid=1' # changelist page, without parameters, and pass an 'invalid=1'
@ -1532,6 +1542,32 @@ class ModelAdmin(BaseModelAdmin):
"admin/object_history.html" "admin/object_history.html"
], context, current_app=self.admin_site.name) ], 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): class InlineModelAdmin(BaseModelAdmin):
""" """
@ -1656,12 +1692,11 @@ class InlineModelAdmin(BaseModelAdmin):
return inlineformset_factory(self.parent_model, self.model, **defaults) return inlineformset_factory(self.parent_model, self.model, **defaults)
def get_fieldsets(self, request, obj=None): def get_fields(self, request, obj=None):
if self.declared_fieldsets: if self.fields:
return self.declared_fieldsets return self.fields
form = self.get_formset(request, obj, fields=None).form form = self.get_formset(request, obj, fields=None).form
fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
return [(None, {'fields': fields})]
def get_queryset(self, request): def get_queryset(self, request):
queryset = super(InlineModelAdmin, self).get_queryset(request) queryset = super(InlineModelAdmin, self).get_queryset(request)

View File

@ -661,45 +661,34 @@ a.deletelink:hover {
.object-tools li { .object-tools li {
display: block; display: block;
float: left; float: left;
background: url(../img/tool-left.gif) 0 0 no-repeat; margin-left: 5px;
padding: 0 0 0 8px;
margin-left: 2px;
height: 16px; height: 16px;
} }
.object-tools li:hover { .object-tools a {
background: url(../img/tool-left_over.gif) 0 0 no-repeat; border-radius: 15px;
} }
.object-tools a:link, .object-tools a:visited { .object-tools a:link, .object-tools a:visited {
display: block; display: block;
float: left; float: left;
color: white; color: white;
padding: .1em 14px .1em 8px; padding: .2em 10px;
height: 14px; background: #999;
background: #999 url(../img/tool-right.gif) 100% 0 no-repeat;
} }
.object-tools a:hover, .object-tools li:hover a { .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 { .object-tools a.viewsitelink, .object-tools a.golink {
background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat; background: #999 url(../img/tooltag-arrowright.png) 95% center no-repeat;
padding-right: 28px; padding-right: 26px;
}
.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat;
} }
.object-tools a.addlink { .object-tools a.addlink {
background: #999 url(../img/tooltag-add.gif) top right no-repeat; background: #999 url(../img/tooltag-add.png) 95% center no-repeat;
padding-right: 28px; padding-right: 26px;
}
.object-tools a.addlink:hover {
background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat;
} }
/* OBJECT HISTORY */ /* OBJECT HISTORY */
@ -837,4 +826,3 @@ table#change-history tbody th {
background: #eee url(../img/nav-bg.gif) bottom left repeat-x; background: #eee url(../img/nav-bg.gif) bottom left repeat-x;
color: #666; color: #666;
} }

View File

@ -54,8 +54,8 @@
.selector ul.selector-chooser { .selector ul.selector-chooser {
float: left; float: left;
width: 22px; width: 22px;
height: 50px; background-color: #eee;
background: url(../img/chooser-bg.gif) top center no-repeat; border-radius: 10px;
margin: 10em 5px 0 5px; margin: 10em 5px 0 5px;
padding: 0; padding: 0;
} }
@ -169,7 +169,8 @@ a.active.selector-clearall {
height: 22px; height: 22px;
width: 50px; width: 50px;
margin: 0 0 3px 40%; 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 { .stacked .selector-chooser li {
@ -575,4 +576,3 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
font-size: 11px; font-size: 11px;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

View File

@ -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", (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!1})})})(django.jQuery); [a(this).attr("id")]);return false})})})(django.jQuery);

View File

@ -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("."+ (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("."+
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+ 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("#"+
"-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, 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>");
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, 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,
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-"+ 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,
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"!= 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"))})};
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, 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!=
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(), "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"))});
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); 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);

View File

@ -3,32 +3,37 @@
/* /*
Depends on urlify.js Depends on urlify.js
Populates a selected field with the values of the dependent fields, Populates a selected field with the values of the dependent fields,
URLifies and shortens the string. URLifies and shortens the string.
dependencies - array of dependent fields id's dependencies - array of dependent fields ids
maxLength - maximum length of the URLify'd string maxLength - maximum length of the URLify'd string
*/ */
return this.each(function() { return this.each(function() {
var field = $(this); var prepopulatedField = $(this);
field.data('_changed', false);
field.change(function() {
field.data('_changed', true);
});
var populate = function () { var populate = function () {
// Bail if the fields value has changed // Bail if the field's value has been changed by the user
if (field.data('_changed') == true) return; if (prepopulatedField.data('_changed')) {
return;
}
var values = []; var values = [];
$.each(dependencies, function(i, field) { $.each(dependencies, function(i, field) {
if ($(field).val().length > 0) { field = $(field);
values.push($(field).val()); if (field.val().length > 0) {
} values.push(field.val());
}) }
field.val(URLify(values.join(' '), maxLength)); });
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); })(django.jQuery);

View File

@ -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);

View File

@ -8,12 +8,8 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans 'Password change successful' %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %} {% block content %}
<h1>{% trans 'Password change successful' %}</h1>
<p>{% trans 'Your password was changed.' %}</p> <p>{% trans 'Your password was changed.' %}</p>
{% endblock %} {% endblock %}

View File

@ -9,7 +9,8 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans 'Password change' %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}<div id="content-main"> {% block content %}<div id="content-main">
@ -21,7 +22,6 @@
</p> </p>
{% endif %} {% 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> <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>

View File

@ -8,12 +8,11 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans 'Password reset complete' %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %} {% 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>{% 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> <p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p>

View File

@ -8,14 +8,12 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans 'Password reset' %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %} {% block content %}
{% if validlink %} {% 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> <p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
@ -28,8 +26,6 @@
{% else %} {% 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> <p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %} {% endif %}

View File

@ -8,12 +8,10 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans 'Password reset successful' %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %} {% 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 "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> <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>

View File

@ -8,12 +8,10 @@
</div> </div>
{% endblock %} {% endblock %}
{% block title %}{% trans "Password reset" %}{% endblock %} {% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %} {% 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> <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 %} <form action="" method="post">{% csrf_token %}

View File

@ -180,7 +180,7 @@ def items_for_result(cl, result, form):
first = True first = True
pk = cl.lookup_opts.pk.attname pk = cl.lookup_opts.pk.attname
for field_name in cl.list_display: for field_name in cl.list_display:
row_class = '' row_classes = ['field-%s' % field_name]
try: try:
f, attr, value = lookup_field(field_name, result, cl.model_admin) f, attr, value = lookup_field(field_name, result, cl.model_admin)
except ObjectDoesNotExist: except ObjectDoesNotExist:
@ -188,7 +188,7 @@ def items_for_result(cl, result, form):
else: else:
if f is None: if f is None:
if field_name == 'action_checkbox': if field_name == 'action_checkbox':
row_class = mark_safe(' class="action-checkbox"') row_classes = ['action-checkbox']
allow_tags = getattr(attr, 'allow_tags', False) allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False) boolean = getattr(attr, 'boolean', False)
if boolean: if boolean:
@ -199,7 +199,7 @@ def items_for_result(cl, result, form):
if allow_tags: if allow_tags:
result_repr = mark_safe(result_repr) result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)): if isinstance(value, (datetime.date, datetime.time)):
row_class = mark_safe(' class="nowrap"') row_classes.append('nowrap')
else: else:
if isinstance(f.rel, models.ManyToOneRel): if isinstance(f.rel, models.ManyToOneRel):
field_val = getattr(result, f.name) field_val = getattr(result, f.name)
@ -210,9 +210,10 @@ def items_for_result(cl, result, form):
else: else:
result_repr = display_for_field(value, f) result_repr = display_for_field(value, f)
if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)): if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)):
row_class = mark_safe(' class="nowrap"') row_classes.append('nowrap')
if force_text(result_repr) == '': if force_text(result_repr) == '':
result_repr = mark_safe('&nbsp;') 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 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: if (first and not cl.list_display_links) or field_name in cl.list_display_links:
table_tag = {True:'th', False:'td'}[first] table_tag = {True:'th', False:'td'}[first]

View File

@ -9,7 +9,7 @@ def prepopulated_fields_js(context):
the prepopulated fields for both the admin form and inlines. the prepopulated fields for both the admin form and inlines.
""" """
prepopulated_fields = [] prepopulated_fields = []
if context['add'] and 'adminform' in context: if 'adminform' in context:
prepopulated_fields.extend(context['adminform'].prepopulated_fields) prepopulated_fields.extend(context['adminform'].prepopulated_fields)
if 'inline_admin_formsets' in context: if 'inline_admin_formsets' in context:
for inline_admin_formset in context['inline_admin_formsets']: for inline_admin_formset in context['inline_admin_formsets']:

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
import sys import sys
import warnings import warnings
@ -7,7 +8,6 @@ from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.utils import six from django.utils import six
from django.utils.datastructures import SortedDict
from django.utils.deprecation import RenameMethodsBase from django.utils.deprecation import RenameMethodsBase
from django.utils.encoding import force_str, force_text from django.utils.encoding import force_str, force_text
from django.utils.translation import ugettext, ugettext_lazy from django.utils.translation import ugettext, ugettext_lazy
@ -319,13 +319,13 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
def get_ordering_field_columns(self): 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 # We must cope with more than one column having the same underlying sort
# field, so we base things on column numbers. # field, so we base things on column numbers.
ordering = self._get_default_ordering() ordering = self._get_default_ordering()
ordering_fields = SortedDict() ordering_fields = OrderedDict()
if ORDER_VAR not in self.params: if ORDER_VAR not in self.params:
# for ordering specified on ModelAdmin or model Meta, we don't know # for ordering specified on ModelAdmin or model Meta, we don't know
# the right column numbers absolutely, because there might be more # the right column numbers absolutely, because there might be more

View File

@ -116,6 +116,8 @@ def url_params_from_lookup_dict(lookups):
if lookups and hasattr(lookups, 'items'): if lookups and hasattr(lookups, 'items'):
items = [] items = []
for k, v in lookups.items(): for k, v in lookups.items():
if callable(v):
v = v()
if isinstance(v, (tuple, list)): if isinstance(v, (tuple, list)):
v = ','.join([str(x) for x in v]) v = ','.join([str(x) for x in v])
elif isinstance(v, bool): elif isinstance(v, bool):
@ -285,7 +287,14 @@ class AdminTextInputWidget(forms.TextInput):
final_attrs.update(attrs) final_attrs.update(attrs)
super(AdminTextInputWidget, self).__init__(attrs=final_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): def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'} final_attrs = {'class': 'vURLField'}
if attrs is not None: if attrs is not None:

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import, unicode_literals from __future__ import unicode_literals
import unittest import unittest

View File

@ -1,3 +1,4 @@
from importlib import import_module
import inspect import inspect
import os import os
import re import re
@ -13,7 +14,6 @@ from django.http import Http404
from django.core import urlresolvers from django.core import urlresolvers
from django.contrib.admindocs import utils from django.contrib.admindocs import utils
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.utils.importlib import import_module
from django.utils._os import upath from django.utils._os import upath
from django.utils import six from django.utils import six
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -319,7 +319,7 @@ def load_all_installed_template_libraries():
libraries = [] libraries = []
for library_name in libraries: for library_name in libraries:
try: try:
lib = template.get_library(library_name) template.get_library(library_name)
except template.InvalidTemplateLibrary: except template.InvalidTemplateLibrary:
pass pass

View File

@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect) csrf_protect_m = method_decorator(csrf_protect)
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
class GroupAdmin(admin.ModelAdmin): class GroupAdmin(admin.ModelAdmin):
@ -87,7 +88,7 @@ class UserAdmin(admin.ModelAdmin):
return False return False
return super(UserAdmin, self).lookup_allowed(lookup, value) return super(UserAdmin, self).lookup_allowed(lookup, value)
@sensitive_post_parameters() @sensitive_post_parameters_m
@csrf_protect_m @csrf_protect_m
@transaction.atomic @transaction.atomic
def add_view(self, request, form_url='', extra_context=None): 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, return super(UserAdmin, self).add_view(request, form_url,
extra_context) extra_context)
@sensitive_post_parameters() @sensitive_post_parameters_m
def user_change_password(self, request, id, form_url=''): def user_change_password(self, request, id, form_url=''):
if not self.has_change_permission(request): if not self.has_change_permission(request):
raise PermissionDenied raise PermissionDenied
@ -127,6 +128,8 @@ class UserAdmin(admin.ModelAdmin):
form = self.change_password_form(user, request.POST) form = self.change_password_form(user, request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
change_message = self.construct_change_message(request, form, None)
self.log_change(request, request.user, change_message)
msg = ugettext('Password changed successfully.') msg = ugettext('Password changed successfully.')
messages.success(request, msg) messages.success(request, msg)
return HttpResponseRedirect('..') return HttpResponseRedirect('..')

View File

@ -17,7 +17,9 @@ class ModelBackend(object):
if user.check_password(password): if user.check_password(password):
return user return user
except UserModel.DoesNotExist: 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): def get_group_permissions(self, user_obj, obj=None):
""" """

View File

@ -1,9 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
from django import forms from django import forms
from django.forms.util import flatatt from django.forms.util import flatatt
from django.template import loader from django.template import loader
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.html import format_html, format_html_join from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
@ -191,13 +192,28 @@ class AuthenticationForm(forms.Form):
code='invalid_login', code='invalid_login',
params={'username': self.username_field.verbose_name}, params={'username': self.username_field.verbose_name},
) )
elif not self.user_cache.is_active: else:
raise forms.ValidationError( self.confirm_login_allowed(self.user_cache)
self.error_messages['inactive'],
code='inactive',
)
return self.cleaned_data 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): def get_user_id(self):
if self.user_cache: if self.user_cache:
return self.user_cache.id return self.user_cache.id
@ -214,7 +230,7 @@ class PasswordResetForm(forms.Form):
subject_template_name='registration/password_reset_subject.txt', subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html', email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator, 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 Generates a one-use only link for resetting password and sends to the
user. user.
@ -247,7 +263,12 @@ class PasswordResetForm(forms.Form):
# Email subject *must not* contain newlines # Email subject *must not* contain newlines
subject = ''.join(subject.splitlines()) subject = ''.join(subject.splitlines())
email = loader.render_to_string(email_template_name, c) 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): class SetPasswordForm(forms.Form):
@ -309,7 +330,7 @@ class PasswordChangeForm(SetPasswordForm):
) )
return old_password return old_password
PasswordChangeForm.base_fields = SortedDict([ PasswordChangeForm.base_fields = OrderedDict([
(k, PasswordChangeForm.base_fields[k]) (k, PasswordChangeForm.base_fields[k])
for k in ['old_password', 'new_password1', 'new_password2'] for k in ['old_password', 'new_password1', 'new_password2']
]) ])
@ -350,3 +371,11 @@ class AdminPasswordChangeForm(forms.Form):
if commit: if commit:
self.user.save() self.user.save()
return self.user 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)

View File

@ -2,13 +2,13 @@ from __future__ import unicode_literals
import base64 import base64
import binascii import binascii
from collections import OrderedDict
import hashlib import hashlib
import importlib
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed 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.utils.encoding import force_bytes, force_str, force_text
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import ( from django.utils.crypto import (
@ -172,7 +172,7 @@ class BasePasswordHasher(object):
if isinstance(self.library, (tuple, list)): if isinstance(self.library, (tuple, list)):
name, mod_path = self.library name, mod_path = self.library
else: else:
name = mod_path = self.library mod_path = self.library
try: try:
module = importlib.import_module(mod_path) module = importlib.import_module(mod_path)
except ImportError as e: except ImportError as e:
@ -243,7 +243,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded): def safe_summary(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3) algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm assert algorithm == self.algorithm
return SortedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('iterations'), iterations), (_('iterations'), iterations),
(_('salt'), mask_hash(salt)), (_('salt'), mask_hash(salt)),
@ -320,7 +320,7 @@ class BCryptSHA256PasswordHasher(BasePasswordHasher):
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4) algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
assert algorithm == self.algorithm assert algorithm == self.algorithm
salt, checksum = data[:22], data[22:] salt, checksum = data[:22], data[22:]
return SortedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('work factor'), work_factor), (_('work factor'), work_factor),
(_('salt'), mask_hash(salt)), (_('salt'), mask_hash(salt)),
@ -368,7 +368,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded): def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2) algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm assert algorithm == self.algorithm
return SortedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)), (_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)), (_('hash'), mask_hash(hash)),
@ -396,7 +396,7 @@ class MD5PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded): def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2) algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm assert algorithm == self.algorithm
return SortedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)), (_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)), (_('hash'), mask_hash(hash)),
@ -429,7 +429,7 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded): def safe_summary(self, encoded):
assert encoded.startswith('sha1$$') assert encoded.startswith('sha1$$')
hash = encoded[6:] hash = encoded[6:]
return SortedDict([ return OrderedDict([
(_('algorithm'), self.algorithm), (_('algorithm'), self.algorithm),
(_('hash'), mask_hash(hash)), (_('hash'), mask_hash(hash)),
]) ])
@ -462,7 +462,7 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
return constant_time_compare(encoded, encoded_2) return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded): def safe_summary(self, encoded):
return SortedDict([ return OrderedDict([
(_('algorithm'), self.algorithm), (_('algorithm'), self.algorithm),
(_('hash'), mask_hash(encoded, show=3)), (_('hash'), mask_hash(encoded, show=3)),
]) ])
@ -496,7 +496,7 @@ class CryptPasswordHasher(BasePasswordHasher):
def safe_summary(self, encoded): def safe_summary(self, encoded):
algorithm, salt, data = encoded.split('$', 2) algorithm, salt, data = encoded.split('$', 2)
assert algorithm == self.algorithm assert algorithm == self.algorithm
return SortedDict([ return OrderedDict([
(_('algorithm'), algorithm), (_('algorithm'), algorithm),
(_('salt'), salt), (_('salt'), salt),
(_('hash'), mask_hash(data, show=3)), (_('hash'), mask_hash(data, show=3)),

View File

@ -0,0 +1 @@
<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>

View File

@ -12,6 +12,17 @@ from django.contrib.auth import authenticate, get_user
from django.http import HttpRequest from django.http import HttpRequest
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings 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): class BaseModelBackendTest(object):
@ -107,10 +118,26 @@ class BaseModelBackendTest(object):
self.assertEqual(user.get_all_permissions(), set(['auth.test'])) self.assertEqual(user.get_all_permissions(), set(['auth.test']))
def test_get_all_superuser_permissions(self): 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) user = self.UserModel._default_manager.get(pk=self.superuser.pk)
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all())) 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 @skipIfCustomUser
class ModelBackendTest(BaseModelBackendTest, TestCase): class ModelBackendTest(BaseModelBackendTest, TestCase):

View File

@ -161,7 +161,7 @@ class AuthContextProcessorTests(TestCase):
# Exception RuntimeError: 'maximum recursion depth exceeded while # Exception RuntimeError: 'maximum recursion depth exceeded while
# calling a Python object' in <type 'exceptions.AttributeError'> # calling a Python object' in <type 'exceptions.AttributeError'>
# ignored" # 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 # Tests for user equality. This is hard because User defines
# equality in a non-duck-typing way # equality in a non-duck-typing way

View File

@ -1,7 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
@ -131,6 +133,40 @@ class AuthenticationFormTest(TestCase):
self.assertEqual(form.non_field_errors(), self.assertEqual(form.non_field_errors(),
[force_text(form.error_messages['inactive'])]) [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): def test_success(self):
# The success case # The success case
data = { data = {
@ -272,7 +308,7 @@ class UserChangeFormTest(TestCase):
fields = ('groups',) fields = ('groups',)
# Just check we can create it # Just check we can create it
form = MyUserForm({}) MyUserForm({})
def test_unsuable_password(self): def test_unsuable_password(self):
user = User.objects.get(username='empty_password') user = User.objects.get(username='empty_password')
@ -417,6 +453,60 @@ class PasswordResetFormTest(TestCase):
form.save() form.save()
self.assertEqual(len(mail.outbox), 0) 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): class ReadOnlyPasswordHashTest(TestCase):

View File

@ -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>')

View File

@ -8,6 +8,7 @@ except ImportError: # Python 2
from django.conf import global_settings, settings from django.conf import global_settings, settings
from django.contrib.sites.models import Site, RequestSite from django.contrib.sites.models import Site, RequestSite
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import mail from django.core import mail
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
@ -54,6 +55,11 @@ class AuthViewsTestCase(TestCase):
self.assertTrue(SESSION_KEY in self.client.session) self.assertTrue(SESSION_KEY in self.client.session)
return response 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): def assertFormError(self, response, error):
"""Assert that error is found in response.context['form'] errors""" """Assert that error is found in response.context['form'] errors"""
form_errors = list(itertools.chain(*response.context['form'].errors.values())) form_errors = list(itertools.chain(*response.context['form'].errors.values()))
@ -122,6 +128,25 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
self.assertTrue("http://" in mail.outbox[0].body) self.assertTrue("http://" in mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) 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): 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." "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): def _test_confirm_start(self):
# Start by creating the email # 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) self.assertEqual(len(mail.outbox), 1)
return self._read_signup_email(mail.outbox[0]) return self._read_signup_email(mail.outbox[0])
@ -322,7 +347,7 @@ class ChangePasswordTest(AuthViewsTestCase):
}) })
def logout(self): def logout(self):
response = self.client.get('/logout/') self.client.get('/logout/')
def test_password_change_fails_with_invalid_old_password(self): def test_password_change_fails_with_invalid_old_password(self):
self.login() self.login()
@ -344,7 +369,7 @@ class ChangePasswordTest(AuthViewsTestCase):
def test_password_change_succeeds(self): def test_password_change_succeeds(self):
self.login() self.login()
response = self.client.post('/password_change/', { self.client.post('/password_change/', {
'old_password': 'password', 'old_password': 'password',
'new_password1': 'password1', 'new_password1': 'password1',
'new_password2': 'password1', 'new_password2': 'password1',
@ -459,7 +484,7 @@ class LoginTest(AuthViewsTestCase):
def test_login_form_contains_request(self): def test_login_form_contains_request(self):
# 15198 # 15198
response = self.client.post('/custom_requestauth_login/', { self.client.post('/custom_requestauth_login/', {
'username': 'testclient', 'username': 'testclient',
'password': 'password', 'password': 'password',
}, follow=True) }, follow=True)
@ -670,18 +695,70 @@ class LogoutTest(AuthViewsTestCase):
self.confirm_logged_out() self.confirm_logged_out()
@skipIfCustomUser @skipIfCustomUser
@override_settings(
PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
)
class ChangelistTests(AuthViewsTestCase): class ChangelistTests(AuthViewsTestCase):
urls = 'django.contrib.auth.tests.urls_admin' urls = 'django.contrib.auth.tests.urls_admin'
def setUp(self):
# Make me a superuser before logging in.
User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True)
self.login()
self.admin = User.objects.get(pk=1)
def get_user_data(self, user):
return {
'username': user.username,
'password': user.password,
'email': user.email,
'is_active': user.is_active,
'is_staff': user.is_staff,
'is_superuser': user.is_superuser,
'last_login_0': user.last_login.strftime('%Y-%m-%d'),
'last_login_1': user.last_login.strftime('%H:%M:%S'),
'initial-last_login_0': user.last_login.strftime('%Y-%m-%d'),
'initial-last_login_1': user.last_login.strftime('%H:%M:%S'),
'date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
'date_joined_1': user.date_joined.strftime('%H:%M:%S'),
'initial-date_joined_0': user.date_joined.strftime('%Y-%m-%d'),
'initial-date_joined_1': user.date_joined.strftime('%H:%M:%S'),
'first_name': user.first_name,
'last_name': user.last_name,
}
# #20078 - users shouldn't be allowed to guess password hashes via # #20078 - users shouldn't be allowed to guess password hashes via
# repeated password__startswith queries. # repeated password__startswith queries.
def test_changelist_disallows_password_lookups(self): def test_changelist_disallows_password_lookups(self):
# Make me a superuser before loging in.
User.objects.filter(username='testclient').update(is_staff=True, is_superuser=True)
self.login()
# A lookup that tries to filter on password isn't OK # A lookup that tries to filter on password isn't OK
with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls:
response = self.client.get('/admin/auth/user/?password__startswith=sha1$') response = self.client.get('/admin/auth/user/?password__startswith=sha1$')
self.assertEqual(response.status_code, 400) self.assertEqual(response.status_code, 400)
self.assertEqual(len(logger_calls), 1) 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')

View File

@ -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_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/$', '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/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})/$', (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', 'django.contrib.auth.views.password_reset_confirm',
dict(post_reset_redirect='/custom/')), dict(post_reset_redirect='/custom/')),

View File

@ -7,10 +7,9 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, QueryDict from django.http import HttpResponseRedirect, QueryDict
from django.template.response import TemplateResponse 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.utils.translation import ugettext as _
from django.shortcuts import resolve_url 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.debug import sensitive_post_parameters
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
@ -141,7 +140,8 @@ def password_reset(request, is_admin_site=False,
post_reset_redirect=None, post_reset_redirect=None,
from_email=None, from_email=None,
current_app=None, current_app=None,
extra_context=None): extra_context=None,
html_email_template_name=None):
if post_reset_redirect is None: if post_reset_redirect is None:
post_reset_redirect = reverse('password_reset_done') post_reset_redirect = reverse('password_reset_done')
else: else:
@ -156,6 +156,7 @@ def password_reset(request, is_admin_site=False,
'email_template_name': email_template_name, 'email_template_name': email_template_name,
'subject_template_name': subject_template_name, 'subject_template_name': subject_template_name,
'request': request, 'request': request,
'html_email_template_name': html_email_template_name,
} }
if is_admin_site: if is_admin_site:
opts = dict(opts, domain_override=request.get_host()) opts = dict(opts, domain_override=request.get_host())
@ -165,6 +166,7 @@ def password_reset(request, is_admin_site=False,
form = password_reset_form() form = password_reset_form()
context = { context = {
'form': form, 'form': form,
'title': _('Password reset'),
} }
if extra_context is not None: if extra_context is not None:
context.update(extra_context) context.update(extra_context)
@ -175,7 +177,9 @@ def password_reset(request, is_admin_site=False,
def password_reset_done(request, def password_reset_done(request,
template_name='registration/password_reset_done.html', template_name='registration/password_reset_done.html',
current_app=None, extra_context=None): current_app=None, extra_context=None):
context = {} context = {
'title': _('Password reset successful'),
}
if extra_context is not None: if extra_context is not None:
context.update(extra_context) context.update(extra_context)
return TemplateResponse(request, template_name, 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): if user is not None and token_generator.check_token(user, token):
validlink = True validlink = True
title = _('Enter new password')
if request.method == 'POST': if request.method == 'POST':
form = set_password_form(user, request.POST) form = set_password_form(user, request.POST)
if form.is_valid(): if form.is_valid():
@ -219,8 +224,10 @@ def password_reset_confirm(request, uidb64=None, token=None,
else: else:
validlink = False validlink = False
form = None form = None
title = _('Password reset unsuccessful')
context = { context = {
'form': form, 'form': form,
'title': title,
'validlink': validlink, 'validlink': validlink,
} }
if extra_context is not None: if extra_context is not None:
@ -232,7 +239,8 @@ def password_reset_complete(request,
template_name='registration/password_reset_complete.html', template_name='registration/password_reset_complete.html',
current_app=None, extra_context=None): current_app=None, extra_context=None):
context = { 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: if extra_context is not None:
context.update(extra_context) context.update(extra_context)
@ -261,6 +269,7 @@ def password_change(request,
form = password_change_form(user=request.user) form = password_change_form(user=request.user)
context = { context = {
'form': form, 'form': form,
'title': _('Password change'),
} }
if extra_context is not None: if extra_context is not None:
context.update(extra_context) context.update(extra_context)
@ -272,7 +281,9 @@ def password_change(request,
def password_change_done(request, def password_change_done(request,
template_name='registration/password_change_done.html', template_name='registration/password_change_done.html',
current_app=None, extra_context=None): current_app=None, extra_context=None):
context = {} context = {
'title': _('Password change successful'),
}
if extra_context is not None: if extra_context is not None:
context.update(extra_context) context.update(extra_context)
return TemplateResponse(request, template_name, context, return TemplateResponse(request, template_name, context,

View File

@ -1,10 +1,10 @@
from importlib import import_module
import warnings import warnings
from django.conf import settings from django.conf import settings
from django.core import urlresolvers from django.core import urlresolvers
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.contrib.comments.models import Comment from django.contrib.comments.models import Comment
from django.contrib.comments.forms import CommentForm 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) warnings.warn("django.contrib.comments is deprecated and will be removed before Django 1.8.", DeprecationWarning)

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django import http from django import http
from django.conf import settings from django.conf import settings
from django.contrib import comments from django.contrib import comments

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.contrib import comments from django.contrib import comments

View File

@ -465,10 +465,10 @@ class GenericInlineModelAdmin(InlineModelAdmin):
formset = BaseGenericInlineFormSet formset = BaseGenericInlineFormSet
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
if self.declared_fieldsets: if 'fields' in kwargs:
fields = flatten_fieldsets(self.declared_fieldsets) fields = kwargs.pop('fields')
else: else:
fields = None fields = flatten_fieldsets(self.get_fieldsets(request, obj))
if self.exclude is None: if self.exclude is None:
exclude = [] exclude = []
else: else:

View File

@ -15,6 +15,7 @@ from django.test.utils import override_settings
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
), ),
CSRF_FAILURE_VIEW='django.views.csrf.csrf_failure',
TEMPLATE_DIRS=( TEMPLATE_DIRS=(
os.path.join(os.path.dirname(__file__), 'templates'), os.path.join(os.path.dirname(__file__), 'templates'),
), ),

View File

@ -39,7 +39,7 @@ class FormPreview(object):
""" """
while 1: while 1:
try: try:
f = self.form.base_fields[name] self.form.base_fields[name]
except KeyError: except KeyError:
break # This field name isn't being used by the form. break # This field name isn't being used by the form.
name += '_' name += '_'

View File

@ -2,8 +2,6 @@
This is a URLconf to be loaded by tests.py. Add any URLs needed for tests only. 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.conf.urls import patterns, url
from django.contrib.formtools.tests.tests import TestFormPreview from django.contrib.formtools.tests.tests import TestFormPreview

View File

@ -1,8 +1,8 @@
from datetime import datetime from datetime import datetime
from importlib import import_module
from django.http import HttpRequest from django.http import HttpRequest
from django.conf import settings from django.conf import settings
from django.utils.importlib import import_module
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -1,11 +1,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from importlib import import_module
from django import forms, http from django import forms, http
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.importlib import import_module
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -9,10 +9,9 @@ from django.forms.models import modelformset_factory
from django.http import HttpResponse from django.http import HttpResponse
from django.template import Template, Context from django.template import Template, Context
from django.contrib.auth.models import User
from django.contrib.formtools.wizard.views import WizardView from django.contrib.formtools.wizard.views import WizardView
temp_storage_location = tempfile.mkdtemp(dir=os.environ.get('DJANGO_TEST_TEMP_DIR')) temp_storage_location = tempfile.mkdtemp(dir=os.environ.get('DJANGO_TEST_TEMP_DIR'))
temp_storage = FileSystemStorage(location=temp_storage_location) temp_storage = FileSystemStorage(location=temp_storage_location)

View File

@ -1,3 +1,4 @@
from collections import OrderedDict
import re import re
from django import forms from django import forms
@ -5,7 +6,6 @@ from django.shortcuts import redirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.forms import formsets, ValidationError from django.forms import formsets, ValidationError
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import six from django.utils import six
@ -17,7 +17,7 @@ from django.contrib.formtools.wizard.forms import ManagementForm
def normalize_name(name): 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') >>> normalize_name('oneTwoThree')
'one_two_three' 'one_two_three'
@ -158,7 +158,7 @@ class WizardView(TemplateView):
form_list = form_list or kwargs.pop('form_list', form_list = form_list or kwargs.pop('form_list',
getattr(cls, 'form_list', None)) or [] 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' 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 The form_list is always generated on the fly because condition methods
could use data from other (maybe previous forms). 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): for form_key, form_class in six.iteritems(self.form_list):
# try to fetch the value from condition list, by default, the form # try to fetch the value from condition list, by default, the form
# gets passed to the new list. # gets passed to the new list.
@ -498,9 +498,10 @@ class WizardView(TemplateView):
if step is None: if step is None:
step = self.steps.current step = self.steps.current
form_list = self.get_form_list() form_list = self.get_form_list()
key = form_list.keyOrder.index(step) + 1 keys = list(form_list.keys())
if len(form_list.keyOrder) > key: key = keys.index(step) + 1
return form_list.keyOrder[key] if len(keys) > key:
return keys[key]
return None return None
def get_prev_step(self, step=None): def get_prev_step(self, step=None):
@ -512,9 +513,10 @@ class WizardView(TemplateView):
if step is None: if step is None:
step = self.steps.current step = self.steps.current
form_list = self.get_form_list() 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: if key >= 0:
return form_list.keyOrder[key] return keys[key]
return None return None
def get_step_index(self, step=None): def get_step_index(self, step=None):
@ -524,7 +526,7 @@ class WizardView(TemplateView):
""" """
if step is None: if step is None:
step = self.steps.current 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): def get_context_data(self, form, **kwargs):
""" """

View File

@ -55,7 +55,7 @@ class GeoModelAdmin(ModelAdmin):
3D editing). 3D editing).
""" """
if isinstance(db_field, models.GeometryField) and db_field.dim < 3: 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. # Setting the widget with the newly defined widget.
kwargs['widget'] = self.get_map_widget(db_field) kwargs['widget'] = self.get_map_widget(db_field)
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)

View File

@ -1,10 +1,12 @@
import os import os
from django.conf import settings from django.conf import settings
from django.core.cache import get_cache from django.core.cache import get_cache
from django.core.cache.backends.db import BaseDatabaseCache from django.core.cache.backends.db import BaseDatabaseCache
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.creation import DatabaseCreation
class SpatiaLiteCreation(DatabaseCreation): class SpatiaLiteCreation(DatabaseCreation):
def create_test_db(self, verbosity=1, autoclobber=False): def create_test_db(self, verbosity=1, autoclobber=False):
@ -53,8 +55,6 @@ class SpatiaLiteCreation(DatabaseCreation):
interactive=False, interactive=False,
database=self.connection.alias) 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: for cache_alias in settings.CACHES:
cache = get_cache(cache_alias) cache = get_cache(cache_alias)
if isinstance(cache, BaseDatabaseCache): if isinstance(cache, BaseDatabaseCache):
@ -62,7 +62,7 @@ class SpatiaLiteCreation(DatabaseCreation):
# Get a cursor (even though we don't need one yet). This has # Get a cursor (even though we don't need one yet). This has
# the side effect of initializing the test database. # the side effect of initializing the test database.
cursor = self.connection.cursor() self.connection.cursor()
return test_database_name return test_database_name

View File

@ -148,6 +148,7 @@ class GeometryField(Field):
value properly, and preserve any other lookup parameters before value properly, and preserve any other lookup parameters before
returning to the caller. returning to the caller.
""" """
value = super(GeometryField, self).get_prep_value(value)
if isinstance(value, SQLEvaluator): if isinstance(value, SQLEvaluator):
return value return value
elif isinstance(value, (tuple, list)): elif isinstance(value, (tuple, list)):

View File

@ -101,7 +101,7 @@ class OGRGeometry(GDALBase):
else: else:
# Seeing if the input is a valid short-hand string # Seeing if the input is a valid short-hand string
# (e.g., 'Point', 'POLYGON'). # (e.g., 'Point', 'POLYGON').
ogr_t = OGRGeomType(geom_input) OGRGeomType(geom_input)
g = capi.create_geom(OGRGeomType(geom_input).num) g = capi.create_geom(OGRGeomType(geom_input).num)
elif isinstance(geom_input, memoryview): elif isinstance(geom_input, memoryview):
# WKB was passed in # WKB was passed in
@ -341,7 +341,7 @@ class OGRGeometry(GDALBase):
sz = self.wkb_size sz = self.wkb_size
# Creating the unsigned character buffer, and passing it in by reference. # Creating the unsigned character buffer, and passing it in by reference.
buf = (c_ubyte * sz)() 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. # Returning a buffer of the string at the pointer.
return memoryview(string_at(buf, sz)) return memoryview(string_at(buf, sz))

View File

@ -22,9 +22,9 @@ class EnvelopeTest(unittest.TestCase):
def test01_init(self): def test01_init(self):
"Testing Envelope initilization." "Testing Envelope initilization."
e1 = Envelope((0, 0, 5, 5)) e1 = Envelope((0, 0, 5, 5))
e2 = Envelope(0, 0, 5, 5) Envelope(0, 0, 5, 5)
e3 = Envelope(0, '0', '5', 5) # Thanks to ww for this Envelope(0, '0', '5', 5) # Thanks to ww for this
e4 = Envelope(e1._envelope) Envelope(e1._envelope)
self.assertRaises(OGRException, Envelope, (5, 5, 0, 0)) self.assertRaises(OGRException, Envelope, (5, 5, 0, 0))
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)) self.assertRaises(OGRException, Envelope, (0, 0, 5, 5, 3))

View File

@ -25,15 +25,12 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
"Testing OGRGeomType object." "Testing OGRGeomType object."
# OGRGeomType should initialize on all these inputs. # OGRGeomType should initialize on all these inputs.
try: OGRGeomType(1)
g = OGRGeomType(1) OGRGeomType(7)
g = OGRGeomType(7) OGRGeomType('point')
g = OGRGeomType('point') OGRGeomType('GeometrycollectioN')
g = OGRGeomType('GeometrycollectioN') OGRGeomType('LINearrING')
g = OGRGeomType('LINearrING') OGRGeomType('Unknown')
g = OGRGeomType('Unknown')
except:
self.fail('Could not create an OGRGeomType object!')
# Should throw TypeError on this input # Should throw TypeError on this input
self.assertRaises(OGRException, OGRGeomType, 23) self.assertRaises(OGRException, OGRGeomType, 23)
@ -127,7 +124,7 @@ class OGRGeomTest(unittest.TestCase, TestDataMixin):
def test02_points(self): def test02_points(self):
"Testing Point objects." "Testing Point objects."
prev = OGRGeometry('POINT(0 0)') OGRGeometry('POINT(0 0)')
for p in self.geometries.points: for p in self.geometries.points:
if not hasattr(p, 'z'): # No 3D if not hasattr(p, 'z'): # No 3D
pnt = OGRGeometry(p.wkt) 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))') 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) self.assertEqual(8, poly.point_count)
with self.assertRaises(OGRException): with self.assertRaises(OGRException):
_ = poly.centroid poly.centroid
poly.close_rings() poly.close_rings()
self.assertEqual(10, poly.point_count) # Two closing points should've been added 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): def test08_multipolygons(self):
"Testing MultiPolygon objects." "Testing MultiPolygon objects."
prev = OGRGeometry('POINT(0 0)') OGRGeometry('POINT(0 0)')
for mp in self.geometries.multipolygons: for mp in self.geometries.multipolygons:
mpoly = OGRGeometry(mp.wkt) mpoly = OGRGeometry(mp.wkt)
self.assertEqual(6, mpoly.geom_type) self.assertEqual(6, mpoly.geom_type)

View File

@ -58,7 +58,7 @@ class SpatialRefTest(unittest.TestCase):
def test01_wkt(self): def test01_wkt(self):
"Testing initialization on valid OGC WKT." "Testing initialization on valid OGC WKT."
for s in srlist: for s in srlist:
srs = SpatialReference(s.wkt) SpatialReference(s.wkt)
def test02_bad_wkt(self): def test02_bad_wkt(self):
"Testing initialization on invalid WKT." "Testing initialization on invalid WKT."
@ -150,7 +150,7 @@ class SpatialRefTest(unittest.TestCase):
target = SpatialReference('WGS84') target = SpatialReference('WGS84')
for s in srlist: for s in srlist:
if s.proj: if s.proj:
ct = CoordTransform(SpatialReference(s.wkt), target) CoordTransform(SpatialReference(s.wkt), target)
def test13_attr_value(self): def test13_attr_value(self):
"Testing the attr_value() method." "Testing the attr_value() method."

View File

@ -11,8 +11,6 @@
Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory
corresponding to settings.GEOIP_PATH. corresponding to settings.GEOIP_PATH.
""" """
from __future__ import absolute_import
try: try:
from .base import GeoIP, GeoIPException from .base import GeoIP, GeoIPException
HAS_GEOIP = True HAS_GEOIP = True

View File

@ -18,7 +18,8 @@ free_regex = re.compile(r'^GEO-\d{3}FREE')
lite_regex = re.compile(r'^GEO-\d{3}LITE') lite_regex = re.compile(r'^GEO-\d{3}LITE')
#### GeoIP classes #### #### GeoIP classes ####
class GeoIPException(Exception): pass class GeoIPException(Exception):
pass
class GeoIP(object): class GeoIP(object):
# The flags for GeoIP memory caching. # The flags for GeoIP memory caching.

View File

@ -14,7 +14,7 @@ class GeoIPRecord(Structure):
('longitude', c_float), ('longitude', c_float),
# TODO: In 1.4.6 this changed from `int dma_code;` to # TODO: In 1.4.6 this changed from `int dma_code;` to
# `union {int metro_code; int dma_code;};`. Change # `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. # pre-1.4.6 versions are no longer distributed.
('dma_code', c_int), ('dma_code', c_int),
('area_code', c_int), ('area_code', c_int),

View File

@ -1,11 +1,12 @@
from importlib import import_module
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos') geom_backend = getattr(settings, 'GEOMETRY_BACKEND', 'geos')
try: 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: except ImportError:
try: try:
module = import_module(geom_backend) module = import_module(geom_backend)

View File

@ -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.coordseq import GEOSCoordSeq
from django.contrib.gis.geos.error import GEOSException, GEOSIndexError 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.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 # All other functions in this module come from the ctypes
# prototypes module -- which handles all interaction with # prototypes module -- which handles all interaction with

View File

@ -124,24 +124,16 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
self.assertEqual(hexewkb_3d, pnt_3d.hexewkb) self.assertEqual(hexewkb_3d, pnt_3d.hexewkb)
self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz) self.assertEqual(True, GEOSGeometry(hexewkb_3d).hasz)
else: else:
try: with self.assertRaises(GEOSException):
hexewkb = pnt_3d.hexewkb pnt_3d.hexewkb
except GEOSException:
pass
else:
self.fail('Should have raised GEOSException.')
# Same for EWKB. # Same for EWKB.
self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb) self.assertEqual(memoryview(a2b_hex(hexewkb_2d)), pnt_2d.ewkb)
if GEOS_PREPARE: if GEOS_PREPARE:
self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb) self.assertEqual(memoryview(a2b_hex(hexewkb_3d)), pnt_3d.ewkb)
else: else:
try: with self.assertRaises(GEOSException):
ewkb = pnt_3d.ewkb pnt_3d.ewkb
except GEOSException:
pass
else:
self.fail('Should have raised GEOSException')
# Redundant sanity check. # Redundant sanity check.
self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid)
@ -158,7 +150,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
# string-based # string-based
for err in self.geometries.errors: for err in self.geometries.errors:
with self.assertRaises((GEOSException, ValueError)): with self.assertRaises((GEOSException, ValueError)):
_ = fromstr(err.wkt) fromstr(err.wkt)
# Bad WKB # Bad WKB
self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0')) self.assertRaises(GEOSException, GEOSGeometry, memoryview(b'0'))

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import, unicode_literals from __future__ import unicode_literals
import os import os
import re import re

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from django.contrib.gis import feeds from django.contrib.gis import feeds

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
from django.contrib.gis.sitemaps import GeoRSSSitemap, KMLSitemap, KMZSitemap from django.contrib.gis.sitemaps import GeoRSSSitemap, KMLSitemap, KMZSitemap
from .feeds import feed_dict from .feeds import feed_dict

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from unittest import skipUnless from unittest import skipUnless
from xml.dom import minidom from xml.dom import minidom

View File

@ -1,5 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from __future__ import absolute_import, unicode_literals from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from io import BytesIO from io import BytesIO
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
import re import re
import unittest import unittest

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from django.conf.urls import patterns from django.conf.urls import patterns

View File

@ -1,7 +1,7 @@
""" """
Tests for geography support in PostGIS 1.5+ Tests for geography support in PostGIS 1.5+
""" """
from __future__ import absolute_import from __future__ import unicode_literals
import os import os
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
import os import os
from unittest import skipUnless from unittest import skipUnless

View File

@ -1,5 +1,5 @@
# coding: utf-8 # coding: utf-8
from __future__ import absolute_import, unicode_literals from __future__ import unicode_literals
from copy import copy from copy import copy
from decimal import Decimal from decimal import Decimal

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import unicode_literals
from unittest import skipUnless from unittest import skipUnless

View File

@ -24,7 +24,7 @@ test_srs = ({'srid' : 4326,
'srtext' : 'PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980"', '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 ' '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'\+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', 'spheroid' : 'GRS 1980', 'name' : 'NAD83 / Texas South Central',
'geographic' : False, 'projected' : True, 'spatialite' : False, 'geographic' : False, 'projected' : True, 'spatialite' : False,
'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only) 'ellipsoid' : (6378137.0, 6356752.31414, 298.257222101), # From proj's "cs2cs -le" and Wikipedia (semi-minor only)

View File

@ -1,4 +1,2 @@
from __future__ import absolute_import
from django.contrib.messages.api import * from django.contrib.messages.api import *
from django.contrib.messages.constants import * from django.contrib.messages.constants import *

View File

@ -1,6 +1,7 @@
from importlib import import_module
from django.conf import settings from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.utils.importlib import import_module
class Command(NoArgsCommand): class Command(NoArgsCommand):

View File

@ -1,9 +1,9 @@
from importlib import import_module
import time import time
from django.conf import settings from django.conf import settings
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
from django.utils.http import cookie_date from django.utils.http import cookie_date
from django.utils.importlib import import_module
class SessionMiddleware(object): class SessionMiddleware(object):
def __init__(self): def __init__(self):

View File

@ -86,17 +86,27 @@ class Sitemap(object):
domain = site.domain domain = site.domain
urls = [] urls = []
latest_lastmod = None
all_items_lastmod = True # track if all items have a lastmod
for item in self.paginator.page(page).object_list: for item in self.paginator.page(page).object_list:
loc = "%s://%s%s" % (protocol, domain, self.__get('location', item)) loc = "%s://%s%s" % (protocol, domain, self.__get('location', item))
priority = self.__get('priority', item, None) 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 = { url_info = {
'item': item, 'item': item,
'location': loc, 'location': loc,
'lastmod': self.__get('lastmod', item, None), 'lastmod': lastmod,
'changefreq': self.__get('changefreq', item, None), 'changefreq': self.__get('changefreq', item, None),
'priority': str(priority if priority is not None else ''), 'priority': str(priority if priority is not None else ''),
} }
urls.append(url_info) urls.append(url_info)
if all_items_lastmod:
self.latest_lastmod = latest_lastmod
return urls return urls
class FlatPageSitemap(Sitemap): class FlatPageSitemap(Sitemap):

View File

@ -77,6 +77,21 @@ class HTTPSitemapTests(SitemapTestsBase):
""" % (self.base_url, date.today()) """ % (self.base_url, date.today())
self.assertXMLEqual(response.content.decode('utf-8'), expected_content) 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") @skipUnless(settings.USE_I18N, "Internationalization is not enabled")
@override_settings(USE_L10N=True) @override_settings(USE_L10N=True)
def test_localized_priority(self): def test_localized_priority(self):

View File

@ -15,10 +15,36 @@ class SimpleSitemap(Sitemap):
def items(self): def items(self):
return [object()] 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_sitemaps = {
'simple': SimpleSitemap, 'simple': SimpleSitemap,
} }
fixed_lastmod_sitemaps = {
'fixed-lastmod': FixedLastmodSitemap,
}
fixed_lastmod__mixed_sitemaps = {
'fixed-lastmod-mixed': FixedLastmodMixedSitemap,
}
generic_sitemaps = { generic_sitemaps = {
'generic': GenericSitemap({'queryset': TestModel.objects.all()}), '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/sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps}),
(r'^simple/custom-sitemap\.xml$', 'sitemap', (r'^simple/custom-sitemap\.xml$', 'sitemap',
{'sitemaps': simple_sitemaps, 'template_name': 'custom_sitemap.xml'}), {'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'^generic/sitemap\.xml$', 'sitemap', {'sitemaps': generic_sitemaps}),
(r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}), (r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}),
url(r'^cached/index\.xml$', cache_page(1)(views.index), url(r'^cached/index\.xml$', cache_page(1)(views.index),

View File

@ -1,3 +1,4 @@
from calendar import timegm
from functools import wraps from functools import wraps
from django.contrib.sites.models import get_current_site 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.http import Http404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import six from django.utils import six
from django.utils.http import http_date
def x_robots_tag(func): def x_robots_tag(func):
@wraps(func) @wraps(func)
@ -64,5 +66,11 @@ def sitemap(request, sitemaps, section=None,
raise Http404("Page %s empty" % page) raise Http404("Page %s empty" % page)
except PageNotAnInteger: except PageNotAnInteger:
raise Http404("No page '%s'" % page) raise Http404("No page '%s'" % page)
return TemplateResponse(request, template_name, {'urlset': urls}, response = TemplateResponse(request, template_name, {'urlset': urls},
content_type=content_type) 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

View File

@ -1,8 +1,9 @@
from collections import OrderedDict
import os import os
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.storage import default_storage, Storage, FileSystemStorage 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.functional import empty, memoize, LazyObject
from django.utils.module_loading import import_by_path from django.utils.module_loading import import_by_path
from django.utils._os import safe_join 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 import utils
from django.contrib.staticfiles.storage import AppStaticStorage from django.contrib.staticfiles.storage import AppStaticStorage
_finders = SortedDict() _finders = OrderedDict()
class BaseFinder(object): class BaseFinder(object):
@ -47,7 +48,7 @@ class FileSystemFinder(BaseFinder):
# List of locations with static files # List of locations with static files
self.locations = [] self.locations = []
# Maps dir paths to an appropriate storage instance # Maps dir paths to an appropriate storage instance
self.storages = SortedDict() self.storages = OrderedDict()
if not isinstance(settings.STATICFILES_DIRS, (list, tuple)): if not isinstance(settings.STATICFILES_DIRS, (list, tuple)):
raise ImproperlyConfigured( raise ImproperlyConfigured(
"Your STATICFILES_DIRS setting is not a tuple or list; " "Your STATICFILES_DIRS setting is not a tuple or list; "
@ -118,7 +119,7 @@ class AppDirectoriesFinder(BaseFinder):
# The list of apps that are handled # The list of apps that are handled
self.apps = [] self.apps = []
# Mapping of app module paths to storage instances # Mapping of app module paths to storage instances
self.storages = SortedDict() self.storages = OrderedDict()
if apps is None: if apps is None:
apps = settings.INSTALLED_APPS apps = settings.INSTALLED_APPS
for app in apps: for app in apps:

View File

@ -2,12 +2,12 @@ from __future__ import unicode_literals
import os import os
import sys import sys
from collections import OrderedDict
from optparse import make_option from optparse import make_option
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.core.management.base import CommandError, NoArgsCommand from django.core.management.base import CommandError, NoArgsCommand
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.utils.datastructures import SortedDict
from django.utils.six.moves import input from django.utils.six.moves import input
from django.contrib.staticfiles import finders, storage from django.contrib.staticfiles import finders, storage
@ -97,7 +97,7 @@ class Command(NoArgsCommand):
else: else:
handler = self.copy_file handler = self.copy_file
found_files = SortedDict() found_files = OrderedDict()
for finder in finders.get_finders(): for finder in finders.get_finders():
for path, storage in finder.list(self.ignore_patterns): for path, storage in finder.list(self.ignore_patterns):
# Prefix the relative path if the source storage contains it # Prefix the relative path if the source storage contains it

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import OrderedDict
import hashlib import hashlib
from importlib import import_module
import os import os
import posixpath import posixpath
import re import re
@ -15,10 +17,8 @@ from django.core.cache import (get_cache, InvalidCacheBackendError,
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import FileSystemStorage, get_storage_class 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.encoding import force_bytes, force_text
from django.utils.functional import LazyObject from django.utils.functional import LazyObject
from django.utils.importlib import import_module
from django.utils._os import upath from django.utils._os import upath
from django.contrib.staticfiles.utils import check_settings, matches_patterns from django.contrib.staticfiles.utils import check_settings, matches_patterns
@ -64,7 +64,7 @@ class CachedFilesMixin(object):
except InvalidCacheBackendError: except InvalidCacheBackendError:
# Use the default backend # Use the default backend
self.cache = default_cache self.cache = default_cache
self._patterns = SortedDict() self._patterns = OrderedDict()
for extension, patterns in self.patterns: for extension, patterns in self.patterns:
for pattern in patterns: for pattern in patterns:
if isinstance(pattern, (tuple, list)): if isinstance(pattern, (tuple, list)):
@ -202,7 +202,7 @@ class CachedFilesMixin(object):
def post_process(self, paths, dry_run=False, **options): 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: Processing is actually two separate operations:

View File

@ -11,7 +11,6 @@ except ImportError: # Python 2
from urllib import unquote from urllib import unquote
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import Http404 from django.http import Http404
from django.views import static 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. It uses the django.views.static view to serve the found files.
""" """
if not settings.DEBUG and not insecure: if not settings.DEBUG and not insecure:
raise ImproperlyConfigured("The staticfiles view can only be used in " raise Http404
"debug mode or if the --insecure "
"option of 'runserver' is used")
normalized_path = posixpath.normpath(unquote(path)).lstrip('/') normalized_path = posixpath.normpath(unquote(path)).lstrip('/')
absolute_path = finders.find(normalized_path) absolute_path = finders.find(normalized_path)
if not absolute_path: if not absolute_path:

Some files were not shown because too many files have changed in this diff Show More