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
3
AUTHORS
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 199 B |
Before Width: | Height: | Size: 212 B |
Before Width: | Height: | Size: 197 B |
Before Width: | Height: | Size: 203 B |
Before Width: | Height: | Size: 198 B |
Before Width: | Height: | Size: 200 B |
Before Width: | Height: | Size: 932 B |
BIN
django/contrib/admin/static/admin/img/tooltag-add.png
Normal file
After Width: | Height: | Size: 967 B |
Before Width: | Height: | Size: 336 B |
Before Width: | Height: | Size: 351 B |
BIN
django/contrib/admin/static/admin/img/tooltag-arrowright.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 354 B |
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
||||||
|
@ -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(' ')
|
result_repr = mark_safe(' ')
|
||||||
|
row_class = mark_safe(' class="%s"' % ' '.join(row_classes))
|
||||||
# If list_display_links not defined, add the link tag to the first field
|
# If 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]
|
||||||
|
@ -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']:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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('..')
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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)),
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
|
||||||
|
59
django/contrib/auth/tests/test_templates.py
Normal 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>')
|
@ -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')
|
||||||
|
@ -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/')),
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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'),
|
||||||
),
|
),
|
||||||
|
@ -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 += '_'
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)):
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
@ -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."
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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'))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 *
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|