diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-06-28 17:32:57 +0100 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-06-28 17:32:57 +0100 |
| commit | 7a47ba6f6aeca36b8b092dbafd36abb342d34d4b (patch) | |
| tree | e959922f7d4d08057225459e31697f410e26df26 /django | |
| parent | e2d7e83256234251a81ad3388428f6579795a672 (diff) | |
| parent | 48dd1e63bbb93479666208535a56f8c7c4aeab3a (diff) | |
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts:
django/db/backends/__init__.py
django/db/models/fields/related.py
tests/field_deconstruction/tests.py
Diffstat (limited to 'django')
76 files changed, 455 insertions, 227 deletions
diff --git a/django/__init__.py b/django/__init__.py index 5a1c74efa7..b8077e17fa 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,4 +1,4 @@ -VERSION = (1, 6, 0, 'alpha', 1) +VERSION = (1, 7, 0, 'alpha', 0) def get_version(*args, **kwargs): # Don't litter django/__init__.py with all the get_version stuff. diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index a56c6a6168..a4c5828f37 100644 --- a/django/contrib/admin/actions.py +++ b/django/contrib/admin/actions.py @@ -19,7 +19,7 @@ def delete_selected(modeladmin, request, queryset): deleteable objects, or, if the user has no permission one of the related childs (foreignkeys), a "permission denied" message. - Next, it delets all selected objects and redirects back to the change list. + Next, it deletes all selected objects and redirects back to the change list. """ opts = modeladmin.model._meta app_label = opts.app_label diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 942385e1c6..fd516cb512 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -4,18 +4,15 @@ from functools import partial, reduce, update_wrapper from django import forms from django.conf import settings -from django.forms.formsets import all_valid, DELETION_FIELD_NAME -from django.forms.models import (modelform_factory, modelformset_factory, - inlineformset_factory, BaseInlineFormSet, modelform_defines_fields) -from django.contrib.contenttypes.models import ContentType +from django.contrib import messages from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, model_format_dict, NestedObjects, lookup_needs_distinct) from django.contrib.admin import validation from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.templatetags.admin_urls import add_preserved_filters -from django.contrib import messages -from django.views.decorators.csrf import csrf_protect +from django.contrib.auth import get_permission_codename +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied, ValidationError, FieldError from django.core.paginator import Paginator from django.core.urlresolvers import reverse @@ -24,7 +21,10 @@ from django.db.models.constants import LOOKUP_SEP from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist from django.db.models.sql.constants import QUERY_TERMS -from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.forms.formsets import all_valid, DELETION_FIELD_NAME +from django.forms.models import (modelform_factory, modelformset_factory, + inlineformset_factory, BaseInlineFormSet, modelform_defines_fields) +from django.http import Http404, HttpResponseRedirect from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse @@ -39,6 +39,10 @@ from django.utils.text import capfirst, get_text_list from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.utils.encoding import force_text +from django.views.decorators.csrf import csrf_protect + + +IS_POPUP_VAR = '_popup' HORIZONTAL, VERTICAL = 1, 2 # returns the <ul> class for a given radio_admin field @@ -56,15 +60,15 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = { 'form_class': forms.SplitDateTimeField, 'widget': widgets.AdminSplitDateTime }, - models.DateField: {'widget': widgets.AdminDateWidget}, - models.TimeField: {'widget': widgets.AdminTimeWidget}, - models.TextField: {'widget': widgets.AdminTextareaWidget}, - models.URLField: {'widget': widgets.AdminURLFieldWidget}, - models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget}, + models.DateField: {'widget': widgets.AdminDateWidget}, + models.TimeField: {'widget': widgets.AdminTimeWidget}, + models.TextField: {'widget': widgets.AdminTextareaWidget}, + models.URLField: {'widget': widgets.AdminURLFieldWidget}, + models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget}, models.BigIntegerField: {'widget': widgets.AdminBigIntegerFieldWidget}, - models.CharField: {'widget': widgets.AdminTextInputWidget}, - models.ImageField: {'widget': widgets.AdminFileWidget}, - models.FileField: {'widget': widgets.AdminFileWidget}, + models.CharField: {'widget': widgets.AdminTextInputWidget}, + models.ImageField: {'widget': widgets.AdminFileWidget}, + models.FileField: {'widget': widgets.AdminFileWidget}, } csrf_protect_m = method_decorator(csrf_protect) @@ -350,7 +354,8 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): Can be overridden by the user in subclasses. """ opts = self.opts - return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) + codename = get_permission_codename('add', opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) def has_change_permission(self, request, obj=None): """ @@ -364,7 +369,8 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): request has permission to change *any* object of the given type. """ opts = self.opts - return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) + codename = get_permission_codename('change', opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) def has_delete_permission(self, request, obj=None): """ @@ -378,7 +384,9 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): request has permission to delete *any* object of the given type. """ opts = self.opts - return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) + codename = get_permission_codename('delete', opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) + class ModelAdmin(BaseModelAdmin): "Encapsulates all admin options and functionality for a given model." @@ -606,11 +614,11 @@ class ModelAdmin(BaseModelAdmin): """ from django.contrib.admin.models import LogEntry, ADDITION LogEntry.objects.log_action( - user_id = request.user.pk, - content_type_id = ContentType.objects.get_for_model(object).pk, - object_id = object.pk, - object_repr = force_text(object), - action_flag = ADDITION + user_id=request.user.pk, + content_type_id=ContentType.objects.get_for_model(object).pk, + object_id=object.pk, + object_repr=force_text(object), + action_flag=ADDITION ) def log_change(self, request, object, message): @@ -621,12 +629,12 @@ class ModelAdmin(BaseModelAdmin): """ from django.contrib.admin.models import LogEntry, CHANGE LogEntry.objects.log_action( - user_id = request.user.pk, - content_type_id = ContentType.objects.get_for_model(object).pk, - object_id = object.pk, - object_repr = force_text(object), - action_flag = CHANGE, - change_message = message + user_id=request.user.pk, + content_type_id=ContentType.objects.get_for_model(object).pk, + object_id=object.pk, + object_repr=force_text(object), + action_flag=CHANGE, + change_message=message ) def log_deletion(self, request, object, object_repr): @@ -638,11 +646,11 @@ class ModelAdmin(BaseModelAdmin): """ from django.contrib.admin.models import LogEntry, DELETION LogEntry.objects.log_action( - user_id = request.user.pk, - content_type_id = ContentType.objects.get_for_model(self.model).pk, - object_id = object.pk, - object_repr = object_repr, - action_flag = DELETION + user_id=request.user.pk, + content_type_id=ContentType.objects.get_for_model(self.model).pk, + object_id=object.pk, + object_repr=object_repr, + action_flag=DELETION ) def action_checkbox(self, obj): @@ -660,8 +668,8 @@ class ModelAdmin(BaseModelAdmin): """ # If self.actions is explicitly set to None that means that we don't # want *any* actions enabled on this page. - from django.contrib.admin.views.main import IS_POPUP_VAR - if self.actions is None or IS_POPUP_VAR in request.GET: + from django.contrib.admin.views.main import _is_changelist_popup + if self.actions is None or _is_changelist_popup(request): return SortedDict() actions = [] @@ -878,7 +886,7 @@ class ModelAdmin(BaseModelAdmin): 'has_add_permission': self.has_add_permission(request), 'has_change_permission': self.has_change_permission(request, obj), 'has_delete_permission': self.has_delete_permission(request, obj), - 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField, + 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField, 'has_absolute_url': hasattr(self.model, 'get_absolute_url'), 'form_url': form_url, 'opts': opts, @@ -908,12 +916,11 @@ class ModelAdmin(BaseModelAdmin): msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. - if "_popup" in request.POST: - return HttpResponse( - '<!DOCTYPE html><html><head><title></title></head><body>' - '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \ - # escape() calls force_text. - (escape(pk_value), escapejs(obj))) + if IS_POPUP_VAR in request.POST: + return SimpleTemplateResponse('admin/popup_response.html', { + 'pk_value': escape(pk_value), + 'obj': escapejs(obj) + }) elif "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict @@ -1049,7 +1056,7 @@ class ModelAdmin(BaseModelAdmin): if action_form.is_valid(): action = action_form.cleaned_data['action'] select_across = action_form.cleaned_data['select_across'] - func, name, description = self.get_actions(request)[action] + func = self.get_actions(request)[action][0] # Get the list of selected PKs. If nothing's selected, we can't # perform an action on it, so bail. Except we want to perform @@ -1158,7 +1165,7 @@ class ModelAdmin(BaseModelAdmin): context = { 'title': _('Add %s') % force_text(opts.verbose_name), 'adminform': adminForm, - 'is_popup': "_popup" in request.REQUEST, + 'is_popup': IS_POPUP_VAR in request.REQUEST, 'media': media, 'inline_admin_formsets': inline_admin_formsets, 'errors': helpers.AdminErrorList(form, formsets), @@ -1251,7 +1258,7 @@ class ModelAdmin(BaseModelAdmin): 'adminform': adminForm, 'object_id': object_id, 'original': obj, - 'is_popup': "_popup" in request.REQUEST, + 'is_popup': IS_POPUP_VAR in request.REQUEST, 'media': media, 'inline_admin_formsets': inline_admin_formsets, 'errors': helpers.AdminErrorList(form, formsets), @@ -1280,7 +1287,7 @@ class ModelAdmin(BaseModelAdmin): actions = self.get_actions(request) if actions: # Add the action checkboxes if there are any actions available. - list_display = ['action_checkbox'] + list(list_display) + list_display = ['action_checkbox'] + list(list_display) ChangeList = self.get_changelist(request) try: @@ -1429,7 +1436,10 @@ class ModelAdmin(BaseModelAdmin): raise PermissionDenied if obj is None: - raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)}) + raise Http404( + _('%(name)s object with primary key %(key)r does not exist.') % + {'name': force_text(opts.verbose_name), 'key': escape(object_id)} + ) using = router.db_for_write(self.model) @@ -1438,7 +1448,7 @@ class ModelAdmin(BaseModelAdmin): (deleted_objects, perms_needed, protected) = get_deleted_objects( [obj], opts, request.user, self.admin_site, using) - if request.POST: # The user has already confirmed the deletion. + if request.POST: # The user has already confirmed the deletion. if perms_needed: raise PermissionDenied obj_display = force_text(obj) @@ -1456,7 +1466,9 @@ class ModelAdmin(BaseModelAdmin): (opts.app_label, opts.model_name), current_app=self.admin_site.name) preserved_filters = self.get_preserved_filters(request) - post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url) + post_url = add_preserved_filters( + {'preserved_filters': preserved_filters, 'opts': opts}, post_url + ) else: post_url = reverse('admin:index', current_app=self.admin_site.name) @@ -1522,6 +1534,7 @@ class ModelAdmin(BaseModelAdmin): "admin/object_history.html" ], context, current_app=self.admin_site.name) + class InlineModelAdmin(BaseModelAdmin): """ Options for inline editing of ``model`` instances. @@ -1665,8 +1678,7 @@ class InlineModelAdmin(BaseModelAdmin): # to have the change permission for the related model in order to # be able to do anything with the intermediate model. return self.has_change_permission(request) - return request.user.has_perm( - self.opts.app_label + '.' + self.opts.get_add_permission()) + return super(InlineModelAdmin, self).has_add_permission(request) def has_change_permission(self, request, obj=None): opts = self.opts @@ -1677,8 +1689,8 @@ class InlineModelAdmin(BaseModelAdmin): if field.rel and field.rel.to != self.parent_model: opts = field.rel.to._meta break - return request.user.has_perm( - opts.app_label + '.' + opts.get_change_permission()) + codename = get_permission_codename('change', opts) + return request.user.has_perm("%s.%s" % (opts.app_label, codename)) def has_delete_permission(self, request, obj=None): if self.opts.auto_created: @@ -1687,8 +1699,7 @@ class InlineModelAdmin(BaseModelAdmin): # to have the change permission for the related model in order to # be able to do anything with the intermediate model. return self.has_change_permission(request, obj) - return request.user.has_perm( - self.opts.app_label + '.' + self.opts.get_delete_permission()) + return super(InlineModelAdmin, self).has_delete_permission(request, obj) class StackedInline(InlineModelAdmin): diff --git a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js index ce54fa5083..6d1748663e 100644 --- a/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js +++ b/django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js @@ -32,9 +32,9 @@ function showRelatedObjectLookupPopup(triggeringLink) { name = id_to_windowname(name); var href; if (triggeringLink.href.search(/\?/) >= 0) { - href = triggeringLink.href + '&pop=1'; + href = triggeringLink.href + '&_popup=1'; } else { - href = triggeringLink.href + '?pop=1'; + href = triggeringLink.href + '?_popup=1'; } var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); win.focus(); diff --git a/django/contrib/admin/templates/admin/popup_response.html b/django/contrib/admin/templates/admin/popup_response.html new file mode 100644 index 0000000000..44833b2f93 --- /dev/null +++ b/django/contrib/admin/templates/admin/popup_response.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head><title></title></head> + <body> + <script type="text/javascript"> + opener.dismissAddAnotherPopup(window, "{{ pk_value }}", "{{ obj }}"); + </script> + </body> +</html> diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index f5a9e7b8b9..c9b626d18f 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -6,7 +6,7 @@ <input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar" /> <input type="submit" value="{% trans 'Search' %}" /> {% if show_result_count %} - <span class="small quiet">{% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}pop=1{% endif %}">{% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> + <span class="small quiet">{% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}_popup=1{% endif %}">{% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}</a>)</span> {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}"/>{% endifnotequal %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 44ae5850b1..01b3bccbbc 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index e81b13cda4..8596dfb825 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -11,7 +11,7 @@ from django.contrib.admin.templatetags.admin_static import static from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import formats -from django.utils.html import format_html +from django.utils.html import escapejs, format_html from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext as _ @@ -226,12 +226,12 @@ def items_for_result(cl, result, form): else: attr = pk value = result.serializable_value(attr) - result_id = repr(force_text(value))[1:] + result_id = escapejs(value) yield format_html('<{0}{1}><a href="{2}"{3}>{4}</a></{5}>', table_tag, row_class, url, - format_html(' onclick="opener.dismissRelatedLookupPopup(window, {0}); return false;"', result_id) + format_html(' onclick="opener.dismissRelatedLookupPopup(window, '{0}'); return false;"', result_id) if cl.is_popup else '', result_repr, table_tag) diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py index bb1b16883d..19da87d61f 100644 --- a/django/contrib/admin/templatetags/admin_urls.py +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -1,5 +1,3 @@ -from django.utils.http import urlencode - try: from urllib.parse import parse_qsl, urlparse, urlunparse except ImportError: @@ -8,6 +6,7 @@ except ImportError: from django import template from django.contrib.admin.util import quote from django.core.urlresolvers import resolve, Resolver404 +from django.utils.http import urlencode register = template.Library() @@ -47,7 +46,8 @@ def add_preserved_filters(context, url, popup=False): merged_qs.update(preserved_filters) if popup: - merged_qs['_popup'] = 1 + from django.contrib.admin.options import IS_POPUP_VAR + merged_qs[IS_POPUP_VAR] = 1 merged_qs.update(parsed_qs) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 222d433e53..fb9634d114 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -1,8 +1,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.fields import FieldDoesNotExist -from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, - _get_foreign_key) +from django.forms.models import BaseModelForm, BaseModelFormSet, _get_foreign_key from django.contrib.admin.util import get_fields_from_path, NotRelationField """ diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index f676706a89..56462dece8 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -15,7 +15,7 @@ from django.utils.http import urlencode from django.contrib.admin import FieldListFilter from django.contrib.admin.exceptions import DisallowedModelAdminLookup -from django.contrib.admin.options import IncorrectLookupParameters +from django.contrib.admin.options import IncorrectLookupParameters, IS_POPUP_VAR from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -26,7 +26,6 @@ ORDER_TYPE_VAR = 'ot' PAGE_VAR = 'p' SEARCH_VAR = 'q' TO_FIELD_VAR = 't' -IS_POPUP_VAR = 'pop' ERROR_FLAG = 'e' IGNORED_PARAMS = ( @@ -36,6 +35,29 @@ IGNORED_PARAMS = ( EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)') +def _is_changelist_popup(request): + """ + Returns True if the popup GET parameter is set. + + This function is introduced to facilitate deprecating the legacy + value for IS_POPUP_VAR and should be removed at the end of the + deprecation cycle. + """ + + if IS_POPUP_VAR in request.GET: + return True + + IS_LEGACY_POPUP_VAR = 'pop' + if IS_LEGACY_POPUP_VAR in request.GET: + warnings.warn( + "The `%s` GET parameter has been renamed to `%s`." % + (IS_LEGACY_POPUP_VAR, IS_POPUP_VAR), + PendingDeprecationWarning, 2) + return True + + return False + + class RenameChangeListMethods(RenameMethodsBase): renamed_methods = ( ('get_query_set', 'get_queryset', PendingDeprecationWarning), @@ -67,7 +89,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): except ValueError: self.page_num = 0 self.show_all = ALL_VAR in request.GET - self.is_popup = IS_POPUP_VAR in request.GET + self.is_popup = _is_changelist_popup(request) self.to_field = request.GET.get(TO_FIELD_VAR) self.params = dict(request.GET.items()) if PAGE_VAR in self.params: diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index c03883def7..b3faf06e25 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -17,7 +17,6 @@ from django.utils.importlib import import_module from django.utils._os import upath from django.utils import six from django.utils.translation import ugettext as _ -from django.utils.safestring import mark_safe # Exclude methods starting with these strings from documentation MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_') diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 029193d582..2f620a34fe 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -108,7 +108,9 @@ def logout(request): def get_user_model(): - "Return the User model that is active in this project" + """ + Returns the User model that is active in this project. + """ from django.db.models import get_model try: @@ -122,6 +124,10 @@ def get_user_model(): def get_user(request): + """ + Returns the user model instance associated with the given request session. + If no user is retrieved an instance of `AnonymousUser` is returned. + """ from .models import AnonymousUser try: user_id = request.session[SESSION_KEY] @@ -132,3 +138,10 @@ def get_user(request): except (KeyError, AssertionError): user = AnonymousUser() return user + + +def get_permission_codename(action, opts): + """ + Returns the codename of the permission for the specified action. + """ + return '%s_%s' % (action, opts.model_name) diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 409078fa02..5a1db68613 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -1,6 +1,7 @@ from django.db import transaction from django.conf import settings from django.contrib import admin +from django.contrib.admin.options import IS_POPUP_VAR from django.contrib.auth.forms import (UserCreationForm, UserChangeForm, AdminPasswordChangeForm) from django.contrib.auth.models import User, Group @@ -143,7 +144,7 @@ class UserAdmin(admin.ModelAdmin): 'adminForm': adminForm, 'form_url': form_url, 'form': form, - 'is_popup': '_popup' in request.REQUEST, + 'is_popup': IS_POPUP_VAR in request.REQUEST, 'add': True, 'change': False, 'has_delete_permission': False, @@ -170,7 +171,7 @@ class UserAdmin(admin.ModelAdmin): # button except in two scenarios: # * The user has pressed the 'Save and add another' button # * We are adding a user in a popup - if '_addanother' not in request.POST and '_popup' not in request.POST: + if '_addanother' not in request.POST and IS_POPUP_VAR not in request.POST: request.POST['_continue'] = 1 return super(UserAdmin, self).response_add(request, obj, post_url_continue) diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index a9ecba45c2..43f5303b63 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -6,8 +6,9 @@ from django import forms from django.forms.util import flatatt from django.template import loader from django.utils.datastructures import SortedDict +from django.utils.encoding import force_bytes from django.utils.html import format_html, format_html_join -from django.utils.http import int_to_base36 +from django.utils.http import urlsafe_base64_encode from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ @@ -243,7 +244,7 @@ class PasswordResetForm(forms.Form): 'email': user.email, 'domain': domain, 'site_name': site_name, - 'uid': int_to_base36(user.pk), + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), 'user': user, 'token': token_generator.make_token(user), 'protocol': 'https' if use_https else 'http', diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index ecf6c1b635..1f338469f8 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -4,10 +4,10 @@ Creates permissions for all installed apps that need permissions. from __future__ import unicode_literals import getpass -import locale import unicodedata -from django.contrib.auth import models as auth_app, get_user_model +from django.contrib.auth import (models as auth_app, get_permission_codename, + get_user_model) from django.core import exceptions from django.core.management.base import CommandError from django.db import DEFAULT_DB_ALIAS, router @@ -17,10 +17,6 @@ from django.utils import six from django.utils.six.moves import input -def _get_permission_codename(action, opts): - return '%s_%s' % (action, opts.model_name) - - def _get_all_permissions(opts, ctype): """ Returns (codename, name) for all permissions in the given opts. @@ -30,16 +26,18 @@ def _get_all_permissions(opts, ctype): _check_permission_clashing(custom, builtin, ctype) return builtin + custom + def _get_builtin_permissions(opts): """ Returns (codename, name) for all autogenerated permissions. """ perms = [] for action in ('add', 'change', 'delete'): - perms.append((_get_permission_codename(action, opts), + perms.append((get_permission_codename(action, opts), 'Can %s %s' % (action, opts.verbose_name_raw))) return perms + def _check_permission_clashing(custom, builtin, ctype): """ Check that permissions for a model do not clash. Raises CommandError if @@ -59,6 +57,7 @@ def _check_permission_clashing(custom, builtin, ctype): (codename, ctype.app_label, ctype.model_class().__name__)) pool.add(codename) + def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs): try: get_model('auth', 'Permission') diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index f6380b9f24..5fec99a783 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -170,7 +170,8 @@ class BaseUserManager(models.Manager): class UserManager(BaseUserManager): - def create_user(self, username, email=None, password=None, **extra_fields): + def _create_user(self, username, email, password, + is_staff, is_superuser, **extra_fields): """ Creates and saves a User with the given username, email and password. """ @@ -179,20 +180,20 @@ class UserManager(BaseUserManager): raise ValueError('The given username must be set') email = self.normalize_email(email) user = self.model(username=username, email=email, - is_staff=False, is_active=True, is_superuser=False, - last_login=now, date_joined=now, **extra_fields) - + is_staff=is_staff, is_active=True, + is_superuser=is_superuser, last_login=now, + date_joined=now, **extra_fields) user.set_password(password) user.save(using=self._db) return user + def create_user(self, username, email=None, password=None, **extra_fields): + return self._create_user(username, email, password, False, False, + **extra_fields) + def create_superuser(self, username, email, password, **extra_fields): - u = self.create_user(username, email, password, **extra_fields) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save(using=self._db) - return u + return self._create_user(username, email, password, True, True, + **extra_fields) @python_2_unicode_compatible @@ -294,10 +295,12 @@ class PermissionsMixin(models.Model): groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True, help_text=_('The groups this user belongs to. A user will ' 'get all permissions granted to each of ' - 'his/her group.')) + 'his/her group.'), + related_name="user_set", related_query_name="user") user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, - help_text='Specific permissions for this user.') + help_text='Specific permissions for this user.', + related_name="user_set", related_query_name="user") class Meta: abstract = True diff --git a/django/contrib/auth/tests/templates/registration/password_reset_email.html b/django/contrib/auth/tests/templates/registration/password_reset_email.html index 1b9a48255a..baac2fc2dd 100644 --- a/django/contrib/auth/tests/templates/registration/password_reset_email.html +++ b/django/contrib/auth/tests/templates/registration/password_reset_email.html @@ -1 +1 @@ -{{ protocol }}://{{ domain }}/reset/{{ uid }}-{{ token }}/
\ No newline at end of file +{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/ diff --git a/django/contrib/auth/tests/test_custom_user.py b/django/contrib/auth/tests/test_custom_user.py index 0f6ebbda55..394baa3204 100644 --- a/django/contrib/auth/tests/test_custom_user.py +++ b/django/contrib/auth/tests/test_custom_user.py @@ -4,7 +4,9 @@ from django.contrib.auth.models import ( AbstractBaseUser, AbstractUser, UserManager, - PermissionsMixin + PermissionsMixin, + Group, + Permission, ) @@ -81,6 +83,20 @@ class CustomUser(AbstractBaseUser): return self.is_admin +# At this point, temporarily remove the groups and user_permissions M2M +# fields from the AbstractUser class, so they don't clash with the related_name +# that sets. + +old_au_local_m2m = AbstractUser._meta.local_many_to_many +old_pm_local_m2m = PermissionsMixin._meta.local_many_to_many +groups = models.ManyToManyField(Group, blank=True) +groups.contribute_to_class(PermissionsMixin, "groups") +user_permissions = models.ManyToManyField(Permission, blank=True) +user_permissions.contribute_to_class(PermissionsMixin, "user_permissions") +PermissionsMixin._meta.local_many_to_many = [groups, user_permissions] +AbstractUser._meta.local_many_to_many = [groups, user_permissions] + + # The extension user is a simple extension of the built-in user class, # adding a required date_of_birth field. This allows us to check for # any hard references to the name "User" in forms/handlers etc. @@ -178,3 +194,7 @@ class CustomUserBadRequiredFields(AbstractBaseUser): class Meta: app_label = 'auth' + +# Undo swap hack +AbstractUser._meta.local_many_to_many = old_au_local_m2m +PermissionsMixin._meta.local_many_to_many = old_pm_local_m2m diff --git a/django/contrib/auth/tests/test_models.py b/django/contrib/auth/tests/test_models.py index cf412c96e6..b0a4559b42 100644 --- a/django/contrib/auth/tests/test_models.py +++ b/django/contrib/auth/tests/test_models.py @@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, UserManager) from django.contrib.auth.tests.utils import skipIfCustomUser +from django.db.models.signals import post_save from django.test import TestCase from django.test.utils import override_settings from django.utils import six @@ -140,3 +141,27 @@ class IsActiveTestCase(TestCase): user_fetched = UserModel._default_manager.get(pk=user.pk) # the attribute is always true for newly retrieved instance self.assertEqual(user_fetched.is_active, True) + + +@skipIfCustomUser +class TestCreateSuperUserSignals(TestCase): + """ + Simple test case for ticket #20541 + """ + def post_save_listener(self, *args, **kwargs): + self.signals_count += 1 + + def setUp(self): + self.signals_count = 0 + post_save.connect(self.post_save_listener, sender=User) + + def tearDown(self): + post_save.disconnect(self.post_save_listener, sender=User) + + def test_create_user(self): + User.objects.create_user("JohnDoe") + self.assertEqual(self.signals_count, 1) + + def test_create_superuser(self): + User.objects.create_superuser("JohnDoe", "mail@example.com", "1") + self.assertEqual(self.signals_count, 1) diff --git a/django/contrib/auth/tests/test_remote_user.py b/django/contrib/auth/tests/test_remote_user.py index dae61afed5..5c5024eae9 100644 --- a/django/contrib/auth/tests/test_remote_user.py +++ b/django/contrib/auth/tests/test_remote_user.py @@ -3,7 +3,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.backends import RemoteUserBackend -from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.models import User from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import timezone diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index ef305ac8f1..ba06a6af4d 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -13,8 +13,7 @@ from django.core import mail from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict, HttpRequest from django.utils.encoding import force_text -from django.utils.html import escape -from django.utils.http import urlquote +from django.utils.http import int_to_base36, urlsafe_base64_decode, urlquote from django.utils._os import upath from django.test import TestCase from django.test.utils import override_settings, patch_logger @@ -23,7 +22,7 @@ from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, - SetPasswordForm, PasswordResetForm) + SetPasswordForm) from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.auth.views import login as login_view @@ -92,7 +91,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase): ('password_reset', [], {}), ('password_reset_done', [], {}), ('password_reset_confirm', [], { - 'uidb36': 'aaaaaaa', + 'uidb64': 'aaaaaaa', 'token': '1111-aaaaa', }), ('password_reset_complete', [], {}), @@ -194,6 +193,16 @@ class PasswordResetTest(AuthViewsTestCase): # redirect to a 'complete' page: self.assertContains(response, "Please enter your new password") + def test_confirm_valid_base36(self): + # Remove in Django 1.7 + url, path = self._test_confirm_start() + path_parts = path.strip("/").split("/") + # construct an old style (base36) URL by converting the base64 ID + path_parts[1] = int_to_base36(int(urlsafe_base64_decode(path_parts[1]))) + response = self.client.get("/%s/%s-%s/" % tuple(path_parts)) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + def test_confirm_invalid(self): url, path = self._test_confirm_start() # Let's munge the token in the path, but keep the same length, @@ -205,11 +214,21 @@ class PasswordResetTest(AuthViewsTestCase): def test_confirm_invalid_user(self): # Ensure that we get a 200 response for a non-existant user, not a 404 + response = self.client.get('/reset/123456/1-1/') + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_invalid_user_base36(self): + # Remove in Django 1.7 response = self.client.get('/reset/123456-1-1/') self.assertContains(response, "The password reset link was invalid") def test_confirm_overflow_user(self): # Ensure that we get a 200 response for a base36 user id that overflows int + response = self.client.get('/reset/zzzzzzzzzzzzz/1-1/') + self.assertContains(response, "The password reset link was invalid") + + def test_confirm_overflow_user_base36(self): + # Remove in Django 1.7 response = self.client.get('/reset/zzzzzzzzzzzzz-1-1/') self.assertContains(response, "The password reset link was invalid") diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index 835ff41de7..502fc659d4 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -67,10 +67,10 @@ urlpatterns = urlpatterns + patterns('', (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')), (r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')), - (r'^reset/custom/(?P<uidb36>[0-9A-Za-z]{1,13})-(?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', dict(post_reset_redirect='/custom/')), - (r'^reset/custom/named/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + (r'^reset/custom/named/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', dict(post_reset_redirect='password_reset')), (r'^password_change/custom/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='/custom/')), @@ -88,4 +88,3 @@ urlpatterns = urlpatterns + patterns('', (r'^custom_request_auth_login/$', custom_request_auth_login), url(r'^userpage/(.+)/$', userpage, name="userpage"), ) - diff --git a/django/contrib/auth/urls.py b/django/contrib/auth/urls.py index c5e87ed2eb..801d133437 100644 --- a/django/contrib/auth/urls.py +++ b/django/contrib/auth/urls.py @@ -12,7 +12,10 @@ urlpatterns = patterns('', url(r'^password_change/done/$', 'django.contrib.auth.views.password_change_done', name='password_change_done'), url(r'^password_reset/$', 'django.contrib.auth.views.password_reset', name='password_reset'), url(r'^password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'), + # Support old style base36 password reset links; remove in Django 1.7 url(r'^reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm_uidb36'), + url(r'^reset/(?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', name='password_reset_confirm'), url(r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete', name='password_reset_complete'), diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index fe21683323..e9affb33cd 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -7,9 +7,10 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.template.response import TemplateResponse -from django.utils.http import base36_to_int, is_safe_url +from django.utils.http import base36_to_int, is_safe_url, urlsafe_base64_decode, urlsafe_base64_encode from django.utils.translation import ugettext as _ from django.shortcuts import resolve_url +from django.utils.encoding import force_bytes, force_text from django.views.decorators.debug import sensitive_post_parameters from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect @@ -184,7 +185,7 @@ def password_reset_done(request, # Doesn't need csrf_protect since no-one can guess the URL @sensitive_post_parameters() @never_cache -def password_reset_confirm(request, uidb36=None, token=None, +def password_reset_confirm(request, uidb64=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, @@ -195,15 +196,15 @@ def password_reset_confirm(request, uidb36=None, token=None, form for entering a new password. """ UserModel = get_user_model() - assert uidb36 is not None and token is not None # checked by URLconf + assert uidb64 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('password_reset_complete') else: post_reset_redirect = resolve_url(post_reset_redirect) try: - uid_int = base36_to_int(uidb36) - user = UserModel._default_manager.get(pk=uid_int) - except (ValueError, OverflowError, UserModel.DoesNotExist): + uid = urlsafe_base64_decode(uidb64) + user = UserModel._default_manager.get(pk=uid) + except (TypeError, ValueError, OverflowError, UserModel.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): @@ -227,6 +228,14 @@ def password_reset_confirm(request, uidb36=None, token=None, return TemplateResponse(request, template_name, context, current_app=current_app) +def password_reset_confirm_uidb36(request, uidb36=None, **kwargs): + # Support old password reset URLs that used base36 encoded user IDs. + # Remove in Django 1.7 + try: + uidb64 = force_text(urlsafe_base64_encode(force_bytes(base36_to_int(uidb36)))) + except ValueError: + uidb64 = '1' # dummy invalid ID (incorrect padding for base64) + return password_reset_confirm(request, uidb64=uidb64, **kwargs) def password_reset_complete(request, template_name='registration/password_reset_complete.html', diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index bca638182c..391889cd65 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.comments.models import Comment -from django.utils.translation import ugettext_lazy as _, ungettext, ungettext_lazy +from django.utils.translation import ugettext_lazy as _, ungettext_lazy from django.contrib.comments import get_model from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete diff --git a/django/contrib/flatpages/tests/test_csrf.py b/django/contrib/flatpages/tests/test_csrf.py index 59996d9824..cb51c124b8 100644 --- a/django/contrib/flatpages/tests/test_csrf.py +++ b/django/contrib/flatpages/tests/test_csrf.py @@ -1,5 +1,4 @@ import os -from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase, Client diff --git a/django/contrib/flatpages/tests/test_templatetags.py b/django/contrib/flatpages/tests/test_templatetags.py index 1a6f3c2975..23fa8faf2d 100644 --- a/django/contrib/flatpages/tests/test_templatetags.py +++ b/django/contrib/flatpages/tests/test_templatetags.py @@ -1,5 +1,4 @@ import os -from django.conf import settings from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.tests.utils import skipIfCustomUser from django.template import Template, Context, TemplateSyntaxError diff --git a/django/contrib/formtools/tests/tests.py b/django/contrib/formtools/tests/tests.py index 56ef901127..58c59f4041 100644 --- a/django/contrib/formtools/tests/tests.py +++ b/django/contrib/formtools/tests/tests.py @@ -3,15 +3,11 @@ from __future__ import unicode_literals import datetime import os -import pickle -import re import warnings from django import http -from django.conf import settings from django.contrib.formtools import preview, utils from django.test import TestCase -from django.test.html import parse_html from django.test.utils import override_settings from django.utils._os import upath from django.utils import unittest diff --git a/django/contrib/formtools/tests/wizard/test_cookiestorage.py b/django/contrib/formtools/tests/wizard/test_cookiestorage.py index 060e8260b5..4c1ff97475 100644 --- a/django/contrib/formtools/tests/wizard/test_cookiestorage.py +++ b/django/contrib/formtools/tests/wizard/test_cookiestorage.py @@ -1,5 +1,3 @@ -import json - from django.test import TestCase from django.core import signing from django.core.exceptions import SuspiciousOperation diff --git a/django/contrib/gis/admin/widgets.py b/django/contrib/gis/admin/widgets.py index ceb8e9c9bd..056d25952a 100644 --- a/django/contrib/gis/admin/widgets.py +++ b/django/contrib/gis/admin/widgets.py @@ -2,7 +2,6 @@ import logging from django.forms.widgets import Textarea from django.template import loader, Context -from django.templatetags.static import static from django.utils import six from django.utils import translation diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py index 43ae9a0331..4f64ecce29 100644 --- a/django/contrib/gis/db/backends/postgis/creation.py +++ b/django/contrib/gis/db/backends/postgis/creation.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.utils.functional import cached_property diff --git a/django/contrib/gis/db/models/aggregates.py b/django/contrib/gis/db/models/aggregates.py index cd26839eb5..d0fc6d319d 100644 --- a/django/contrib/gis/db/models/aggregates.py +++ b/django/contrib/gis/db/models/aggregates.py @@ -1,5 +1,4 @@ from django.db.models import Aggregate -from django.contrib.gis.db.models.sql import GeomField class Collect(Aggregate): name = 'Collect' diff --git a/django/contrib/gis/geos/tests/test_io.py b/django/contrib/gis/geos/tests/test_io.py index 38ca2e0923..34eeaf95b9 100644 --- a/django/contrib/gis/geos/tests/test_io.py +++ b/django/contrib/gis/geos/tests/test_io.py @@ -4,7 +4,6 @@ import binascii import unittest from django.contrib.gis import memoryview -from django.utils import six from django.utils.unittest import skipUnless from ..import HAS_GEOS diff --git a/django/contrib/gis/management/commands/ogrinspect.py b/django/contrib/gis/management/commands/ogrinspect.py index 6037cc7ea6..44513e3463 100644 --- a/django/contrib/gis/management/commands/ogrinspect.py +++ b/django/contrib/gis/management/commands/ogrinspect.py @@ -1,4 +1,3 @@ -import os from optparse import make_option from django.contrib.gis import gdal from django.core.management.base import LabelCommand, CommandError diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 6320edcff5..d93adbcd4c 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -from datetime import date - from django.contrib.gis.geos import HAS_GEOS from django.contrib.gis.tests.utils import HAS_SPATIAL_DB, mysql, oracle, no_mysql, no_oracle, no_spatialite from django.test import TestCase diff --git a/django/contrib/sitemaps/tests/base.py b/django/contrib/sitemaps/tests/base.py index 099dba7256..8e027d491e 100644 --- a/django/contrib/sitemaps/tests/base.py +++ b/django/contrib/sitemaps/tests/base.py @@ -1,4 +1,3 @@ -from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core.cache import cache from django.db import models diff --git a/django/contrib/sitemaps/tests/urls/http.py b/django/contrib/sitemaps/tests/urls/http.py index 56103f4ec2..a8b804fd4b 100644 --- a/django/contrib/sitemaps/tests/urls/http.py +++ b/django/contrib/sitemaps/tests/urls/http.py @@ -1,7 +1,6 @@ from datetime import datetime from django.conf.urls import patterns, url from django.contrib.sitemaps import Sitemap, GenericSitemap, FlatPageSitemap, views -from django.contrib.auth.models import User from django.views.decorators.cache import cache_page from django.contrib.sitemaps.tests.base import TestModel diff --git a/django/core/compat_checks/__init__.py b/django/core/checks/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/django/core/compat_checks/__init__.py +++ b/django/core/checks/__init__.py diff --git a/django/core/checks/compatibility/__init__.py b/django/core/checks/compatibility/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/core/checks/compatibility/__init__.py diff --git a/django/core/compat_checks/base.py b/django/core/checks/compatibility/base.py index e54b50f287..7fe52d2af9 100644 --- a/django/core/compat_checks/base.py +++ b/django/core/checks/compatibility/base.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import warnings -from django.core.compat_checks import django_1_6_0 +from django.core.checks.compatibility import django_1_6_0 COMPAT_CHECKS = [ diff --git a/django/core/compat_checks/django_1_6_0.py b/django/core/checks/compatibility/django_1_6_0.py index bb0dabedac..1998c5ba77 100644 --- a/django/core/compat_checks/django_1_6_0.py +++ b/django/core/checks/compatibility/django_1_6_0.py @@ -27,7 +27,7 @@ def check_test_runner(): def run_checks(): """ - Required by the ``checksetup`` management command, this returns a list of + Required by the ``check`` management command, this returns a list of messages from all the relevant check functions for this version of Django. """ checks = [ diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 829d6e774e..efec22850b 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,7 +1,6 @@ """ Global Django exception and warning classes. """ -import logging from functools import reduce import operator diff --git a/django/core/files/move.py b/django/core/files/move.py index 4519dedf97..4bd739b4c4 100644 --- a/django/core/files/move.py +++ b/django/core/files/move.py @@ -51,6 +51,10 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove return try: + # If the destination file exists and allow_overwrite is False then raise an IOError + if not allow_overwrite and os.access(new_file_name, os.F_OK): + raise IOError("Destination file %s exists and allow_overwrite is False" % new_file_name) + os.rename(old_file_name, new_file_name) return except OSError: diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index af78d1d269..38d8154ac9 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -103,6 +103,7 @@ class WSGIRequest(http.HttpRequest): content_length = 0 self._stream = LimitedStream(self.environ['wsgi.input'], content_length) self._read_started = False + self.resolver_match = None def _is_secure(self): return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https' diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 77d5e1b264..8fd46aa759 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -3,13 +3,11 @@ import os import sys from optparse import OptionParser, NO_DEFAULT import imp -import warnings from django.core.exceptions import ImproperlyConfigured from django.core.management.base import BaseCommand, CommandError, handle_default_options from django.core.management.color import color_style from django.utils.importlib import import_module -from django.utils._os import upath from django.utils import six # For backwards compatibility: get_version() used to be in this module. diff --git a/django/core/management/commands/checksetup.py b/django/core/management/commands/check.py index d37e826757..05f48c82bc 100644 --- a/django/core/management/commands/checksetup.py +++ b/django/core/management/commands/check.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import warnings -from django.core.compat_checks.base import check_compatibility +from django.core.checks.compatibility.base import check_compatibility from django.core.management.base import NoArgsCommand diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index 060def5d5a..695cad419b 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -402,11 +402,11 @@ class Command(NoArgsCommand): if self.verbosity > 1: self.stdout.write("copying plural forms: %s\n" % m.group('value')) lines = [] - seen = False + found = False for line in msgs.split('\n'): - if not line and not seen: + if not found and (not line or plural_forms_re.search(line)): line = '%s\n' % m.group('value') - seen = True + found = True lines.append(line) msgs = '\n'.join(lines) break diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 893e5c95af..1de508d749 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -105,7 +105,7 @@ class TemplateCommand(BaseCommand): base_name = '%s_name' % app_or_project base_subdir = '%s_template' % app_or_project base_directory = '%s_directory' % app_or_project - if django.VERSION[-1] == 0: + if django.VERSION[-2] != 'final': docs_version = 'dev' else: docs_version = '%d.%d' % django.VERSION[:2] diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 1e78026c40..cd4f7ffb2b 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -3,7 +3,6 @@ Module for abstract serializer/unserializer base classes. """ from django.db import models -from django.utils.encoding import smart_text from django.utils import six class SerializerDoesNotExist(KeyError): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 2fcf6b9604..26ca9a7fa1 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -330,6 +330,15 @@ class BaseDatabaseWrapper(object): self._set_autocommit(autocommit) self.autocommit = autocommit + def set_rollback(self, rollback): + """ + Set or unset the "needs rollback" flag -- for *advanced use* only. + """ + if not self.in_atomic_block: + raise TransactionManagementError( + "needs_rollback doesn't work outside of an 'atomic' block.") + self.needs_rollback = rollback + def validate_no_atomic_block(self): """ Raise an error if an atomic block is active. @@ -623,6 +632,11 @@ class BaseDatabaseFeatures(object): # Does it support CHECK constraints? supports_check_constraints = True + # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value}) + # parameter passing? Note this can be provided by the backend even if not + # supported by the Python driver + supports_paramstyle_pyformat = True + def __init__(self, connection): self.connection = connection diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 3105ad17e3..5fb806807d 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -762,20 +762,37 @@ class FormatStylePlaceholderCursor(object): self.cursor.arraysize = 100 def _format_params(self, params): - return tuple([OracleParam(p, self, True) for p in params]) + try: + return dict((k,OracleParam(v, self, True)) for k,v in params.items()) + except AttributeError: + return tuple([OracleParam(p, self, True) for p in params]) def _guess_input_sizes(self, params_list): - sizes = [None] * len(params_list[0]) - for params in params_list: - for i, value in enumerate(params): - if value.input_size: - sizes[i] = value.input_size - self.setinputsizes(*sizes) + # Try dict handling; if that fails, treat as sequence + if hasattr(params_list[0], 'keys'): + sizes = {} + for params in params_list: + for k, value in params.items(): + if value.input_size: + sizes[k] = value.input_size + self.setinputsizes(**sizes) + else: + # It's not a list of dicts; it's a list of sequences + sizes = [None] * len(params_list[0]) + for params in params_list: + for i, value in enumerate(params): + if value.input_size: + sizes[i] = value.input_size + self.setinputsizes(*sizes) def _param_generator(self, params): - return [p.force_bytes for p in params] + # Try dict handling; if that fails, treat as sequence + if hasattr(params, 'items'): + return dict((k, v.force_bytes) for k,v in params.items()) + else: + return [p.force_bytes for p in params] - def execute(self, query, params=None): + def _fix_for_params(self, query, params): # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it # it does want a trailing ';' but not a trailing '/'. However, these # characters must be included in the original query in case the query @@ -785,10 +802,18 @@ class FormatStylePlaceholderCursor(object): if params is None: params = [] query = convert_unicode(query, self.charset) + elif hasattr(params, 'keys'): + # Handle params as dict + args = dict((k, ":%s"%k) for k in params.keys()) + query = convert_unicode(query % args, self.charset) else: - params = self._format_params(params) + # Handle params as sequence args = [(':arg%d' % i) for i in range(len(params))] query = convert_unicode(query % tuple(args), self.charset) + return query, self._format_params(params) + + def execute(self, query, params=None): + query, params = self._fix_for_params(query, params) self._guess_input_sizes([params]) try: return self.cursor.execute(query, self._param_generator(params)) @@ -799,22 +824,15 @@ class FormatStylePlaceholderCursor(object): raise def executemany(self, query, params=None): - # cx_Oracle doesn't support iterators, convert them to lists - if params is not None and not isinstance(params, (list, tuple)): - params = list(params) - try: - args = [(':arg%d' % i) for i in range(len(params[0]))] - except (IndexError, TypeError): + if not params: # No params given, nothing to do return None - # cx_Oracle wants no trailing ';' for SQL statements. For PL/SQL, it - # it does want a trailing ';' but not a trailing '/'. However, these - # characters must be included in the original query in case the query - # is being passed to SQL*Plus. - if query.endswith(';') or query.endswith('/'): - query = query[:-1] - query = convert_unicode(query % tuple(args), self.charset) - formatted = [self._format_params(i) for i in params] + # uniform treatment for sequences and iterables + params_iter = iter(params) + query, firstparams = self._fix_for_params(query, next(params_iter)) + # we build a list of formatted params; as we're going to traverse it + # more than once, we can't make it lazy by using a generator + formatted = [firstparams]+[self._format_params(p) for p in params_iter] self._guess_input_sizes(formatted) try: return self.cursor.executemany(query, diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index de86758653..bf75459b4a 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -6,7 +6,6 @@ Requires psycopg 2: http://initd.org/projects/psycopg2 import logging import sys -from django.db import utils from django.db.backends import * from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations from django.db.backends.postgresql_psycopg2.client import DatabaseClient @@ -17,7 +16,6 @@ from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.safestring import SafeText, SafeBytes -from django.utils import six from django.utils.timezone import utc try: diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index 819dd7a29e..1bfda6354d 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -1,5 +1,3 @@ -import psycopg2.extensions - from django.db.backends.creation import BaseDatabaseCreation from django.db.backends.util import truncate_name diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index f96757da8b..c5aab84693 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -69,7 +69,7 @@ class DatabaseOperations(BaseDatabaseOperations): # Cast text lookups to text to allow things like filter(x__contains=4) if lookup_type in ('iexact', 'contains', 'icontains', 'startswith', - 'istartswith', 'endswith', 'iendswith'): + 'istartswith', 'endswith', 'iendswith', 'regex', 'iregex'): lookup = "%s::text" # Use UPPER(x) for case-insensitive lookups; it's faster. diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a53d3ee53e..64da0a9f25 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -20,6 +20,7 @@ from django.db.backends.sqlite3.schema import DatabaseSchemaEditor from django.db.models import fields from django.db.models.sql import aggregates from django.utils.dateparse import parse_date, parse_datetime, parse_time +from django.utils.encoding import force_text from django.utils.functional import cached_property from django.utils.safestring import SafeBytes from django.utils import six @@ -103,6 +104,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_foreign_keys = False supports_check_constraints = False autocommits_when_autocommit_is_off = True + supports_paramstyle_pyformat = False @cached_property def uses_savepoints(self): @@ -253,6 +255,9 @@ class DatabaseOperations(BaseDatabaseOperations): and gets dates and datetimes wrong. For consistency with other backends, coerce when required. """ + if value is None: + return None + internal_type = field.get_internal_type() if internal_type == 'DecimalField': return util.typecast_decimal(field.format_number(value)) @@ -526,4 +531,4 @@ def _sqlite_format_dtdelta(dt, conn, days, secs, usecs): return str(dt) def _sqlite_regexp(re_pattern, re_string): - return bool(re.search(re_pattern, re_string)) + return bool(re.search(re_pattern, force_text(re_string))) if re_string is not None else False diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 64890fff47..9f2e01e781 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -137,7 +137,7 @@ class RelatedField(Field): # related object in a table-spanning query. It uses the lower-cased # object_name by default, but this can be overridden with the # "related_name" option. - return self.rel.related_name or self.opts.model_name + return self.rel.related_query_name or self.rel.related_name or self.opts.model_name class RenameRelatedObjectDescriptorMethods(RenameMethodsBase): @@ -824,7 +824,7 @@ class ReverseManyRelatedObjectsDescriptor(object): class ForeignObjectRel(object): def __init__(self, field, to, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): try: to._meta except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT @@ -833,6 +833,7 @@ class ForeignObjectRel(object): self.field = field self.to = to self.related_name = related_name + self.related_query_name = related_query_name self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to self.multiple = True self.parent_link = parent_link @@ -860,10 +861,10 @@ class ForeignObjectRel(object): class ManyToOneRel(ForeignObjectRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): super(ManyToOneRel, self).__init__( field, to, related_name=related_name, limit_choices_to=limit_choices_to, - parent_link=parent_link, on_delete=on_delete) + parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name) self.field_name = field_name def get_related_field(self): @@ -883,21 +884,22 @@ class ManyToOneRel(ForeignObjectRel): class OneToOneRel(ManyToOneRel): def __init__(self, field, to, field_name, related_name=None, limit_choices_to=None, - parent_link=False, on_delete=None): + parent_link=False, on_delete=None, related_query_name=None): super(OneToOneRel, self).__init__(field, to, field_name, related_name=related_name, limit_choices_to=limit_choices_to, - parent_link=parent_link, on_delete=on_delete + parent_link=parent_link, on_delete=on_delete, related_query_name=related_query_name, ) self.multiple = False class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, - symmetrical=True, through=None, db_constraint=True): + symmetrical=True, through=None, db_constraint=True, related_query_name=None): if through and not db_constraint: raise ValueError("Can't supply a through model and db_constraint=False") self.to = to self.related_name = related_name + self.related_query_name = related_query_name if limit_choices_to is None: limit_choices_to = {} self.limit_choices_to = limit_choices_to @@ -931,6 +933,7 @@ class ForeignObject(RelatedField): kwargs['rel'] = ForeignObjectRel( self, to, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), parent_link=kwargs.pop('parent_link', False), on_delete=kwargs.pop('on_delete', CASCADE), @@ -1141,6 +1144,7 @@ class ForeignKey(ForeignObject): kwargs['rel'] = rel_class( self, to, to_field, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), parent_link=kwargs.pop('parent_link', False), on_delete=kwargs.pop('on_delete', CASCADE), @@ -1371,6 +1375,7 @@ class ManyToManyField(RelatedField): kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), + related_query_name=kwargs.pop('related_query_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None), @@ -1388,7 +1393,8 @@ class ManyToManyField(RelatedField): # Handle the simpler arguments if self.rel.db_constraint is not True: kwargs['db_constraint'] = self.db_constraint - del kwargs['help_text'] + if "help_text" in kwargs: + del kwargs['help_text'] # Rel needs more work. rel = self.rel if isinstance(self.rel.to, basestring): diff --git a/django/db/models/options.py b/django/db/models/options.py index 1a9421c0fa..b173da9538 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -422,12 +422,36 @@ class Options(object): return cache def get_add_permission(self): + """ + This method has been deprecated in favor of + `django.contrib.auth.get_permission_codename`. refs #20642 + """ + warnings.warn( + "`Options.get_add_permission` has been deprecated in favor " + "of `django.contrib.auth.get_permission_codename`.", + PendingDeprecationWarning, stacklevel=2) return 'add_%s' % self.model_name def get_change_permission(self): + """ + This method has been deprecated in favor of + `django.contrib.auth.get_permission_codename`. refs #20642 + """ + warnings.warn( + "`Options.get_change_permission` has been deprecated in favor " + "of `django.contrib.auth.get_permission_codename`.", + PendingDeprecationWarning, stacklevel=2) return 'change_%s' % self.model_name def get_delete_permission(self): + """ + This method has been deprecated in favor of + `django.contrib.auth.get_permission_codename`. refs #20642 + """ + warnings.warn( + "`Options.get_delete_permission` has been deprecated in favor " + "of `django.contrib.auth.get_permission_codename`.", + PendingDeprecationWarning, stacklevel=2) return 'delete_%s' % self.model_name def get_all_related_objects(self, local_only=False, include_hidden=False, diff --git a/django/db/models/query.py b/django/db/models/query.py index b0ce25f5b5..27a87a3f65 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1445,7 +1445,10 @@ class RawQuerySet(object): yield instance def __repr__(self): - return "<RawQuerySet: %r>" % (self.raw_query % tuple(self.params)) + text = self.raw_query + if self.params: + text = text % (self.params if hasattr(self.params, 'keys') else tuple(self.params)) + return "<RawQuerySet: %r>" % text def __getitem__(self, k): return list(self)[k] diff --git a/django/db/transaction.py b/django/db/transaction.py index f770f2efa7..95b9ae165e 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -171,6 +171,26 @@ def clean_savepoints(using=None): """ get_connection(using).clean_savepoints() +def get_rollback(using=None): + """ + Gets the "needs rollback" flag -- for *advanced use* only. + """ + return get_connection(using).needs_rollback + +def set_rollback(rollback, using=None): + """ + Sets or unsets the "needs rollback" flag -- for *advanced use* only. + + When `rollback` is `True`, it triggers a rollback when exiting the + innermost enclosing atomic block that has `savepoint=True` (that's the + default). Use this to force a rollback without raising an exception. + + When `rollback` is `False`, it prevents such a rollback. Use this only + after rolling back to a known-good state! Otherwise, you break the atomic + block and data corruption may occur. + """ + return get_connection(using).set_rollback(rollback) + ################################# # Decorators / context managers # ################################# diff --git a/django/forms/fields.py b/django/forms/fields.py index 52bcf9485c..c4bc3fa88c 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -370,14 +370,8 @@ class DecimalField(IntegerField): def widget_attrs(self, widget): attrs = super(DecimalField, self).widget_attrs(widget) - if isinstance(widget, NumberInput): - if self.max_digits is not None: - max_length = self.max_digits + 1 # for the sign - if self.decimal_places is None or self.decimal_places > 0: - max_length += 1 # for the dot - attrs['maxlength'] = max_length - if self.decimal_places: - attrs['step'] = '0.%s1' % ('0' * (self.decimal_places-1)) + if isinstance(widget, NumberInput) and self.decimal_places: + attrs['step'] = '0.%s1' % ('0' * (self.decimal_places - 1)) return attrs diff --git a/django/forms/formsets.py b/django/forms/formsets.py index edd362c595..cb3126e6d7 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -4,8 +4,9 @@ from django.core.exceptions import ValidationError from django.forms import Form from django.forms.fields import IntegerField, BooleanField from django.forms.util import ErrorList -from django.forms.widgets import Media, HiddenInput +from django.forms.widgets import HiddenInput from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils import six from django.utils.six.moves import xrange @@ -55,8 +56,6 @@ class BaseFormSet(object): self.error_class = error_class self._errors = None self._non_form_errors = None - # construct the forms in the formset - self._construct_forms() def __str__(self): return self.as_table() @@ -125,12 +124,14 @@ class BaseFormSet(object): initial_forms = len(self.initial) if self.initial else 0 return initial_forms - def _construct_forms(self): - # instantiate all the forms and put them in self.forms - self.forms = [] + @cached_property + def forms(self): + """ + Instantiate forms at first property access. + """ # DoS protection is included in total_form_count() - for i in xrange(self.total_form_count()): - self.forms.append(self._construct_form(i)) + forms = [self._construct_form(i) for i in xrange(self.total_form_count())] + return forms def _construct_form(self, i, **kwargs): """ diff --git a/django/forms/util.py b/django/forms/util.py index 568cdd1086..0a73320f83 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.conf import settings from django.utils.html import format_html, format_html_join from django.utils.encoding import force_text, python_2_unicode_compatible -from django.utils.safestring import mark_safe from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.utils import six diff --git a/django/forms/widgets.py b/django/forms/widgets.py index aca4a457af..38d1b99b0d 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -5,7 +5,6 @@ HTML Widget classes from __future__ import absolute_import, unicode_literals import copy -import datetime from itertools import chain try: from urllib.parse import urljoin @@ -16,8 +15,8 @@ import warnings from django.conf import settings from django.forms.util import flatatt, to_current_timezone from django.utils.datastructures import MultiValueDict, MergeDict -from django.utils.html import conditional_escape, format_html, format_html_join -from django.utils.translation import ugettext, ugettext_lazy +from django.utils.html import conditional_escape, format_html +from django.utils.translation import ugettext_lazy from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.safestring import mark_safe from django.utils import datetime_safe, formats, six diff --git a/django/http/request.py b/django/http/request.py index 37aa1a355a..b7f9d241a7 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -39,6 +39,10 @@ class HttpRequest(object): _upload_handlers = [] def __init__(self): + # WARNING: The `WSGIRequest` subclass doesn't call `super`. + # Any variable assignment made here should also happen in + # `WSGIRequest.__init__()`. + self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} self.path = '' self.path_info = '' diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 1b5732fbbf..c13715eeb9 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -6,10 +6,8 @@ against request forgeries from other sites. """ from __future__ import unicode_literals -import hashlib import logging import re -import random from django.conf import settings from django.core.urlresolvers import get_callable diff --git a/django/template/base.py b/django/template/base.py index dc6b0c366c..364b428070 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -1101,6 +1101,7 @@ class Library(object): # for decorators that need it e.g. stringfilter if hasattr(filter_func, "_decorated_function"): setattr(filter_func._decorated_function, attr, value) + filter_func._filter_name = name return filter_func else: raise InvalidTemplateLibrary("Unsupported arguments to " diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 4201cfeb67..76c0121126 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import re import random as random_module -import unicodedata from decimal import Decimal, InvalidOperation, Context, ROUND_HALF_UP from functools import wraps from pprint import pformat diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 04e7a37d8e..959de3dea1 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -665,8 +665,9 @@ def do_filter(parser, token): _, rest = token.contents.split(None, 1) filter_expr = parser.compile_filter("var|%s" % (rest)) for func, unused in filter_expr.filters: - if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'): - raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % func.__name__) + filter_name = getattr(func, '_filter_name', None) + if filter_name in ('escape', 'safe'): + raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % filter_name) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) diff --git a/django/test/utils.py b/django/test/utils.py index be586c75a6..591c588933 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -10,6 +10,7 @@ from django.conf import settings, UserSettingsHolder from django.core import mail from django.core.signals import request_started from django.db import reset_queries +from django.http import request from django.template import Template, loader, TemplateDoesNotExist from django.template.loaders import cached from django.test.signals import template_rendered, setting_changed @@ -87,13 +88,16 @@ def setup_test_environment(): - Set the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. """ - Template.original_render = Template._render + Template._original_render = Template._render Template._render = instrumented_test_render - mail.original_email_backend = settings.EMAIL_BACKEND + # Storing previous values in the settings module itself is problematic. + # Store them in arbitrary (but related) modules instead. See #20636. + + mail._original_email_backend = settings.EMAIL_BACKEND settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' - settings._original_allowed_hosts = settings.ALLOWED_HOSTS + request._original_allowed_hosts = settings.ALLOWED_HOSTS settings.ALLOWED_HOSTS = ['*'] mail.outbox = [] @@ -108,14 +112,14 @@ def teardown_test_environment(): - Restoring the email sending functions """ - Template._render = Template.original_render - del Template.original_render + Template._render = Template._original_render + del Template._original_render - settings.EMAIL_BACKEND = mail.original_email_backend - del mail.original_email_backend + settings.EMAIL_BACKEND = mail._original_email_backend + del mail._original_email_backend - settings.ALLOWED_HOSTS = settings._original_allowed_hosts - del settings._original_allowed_hosts + settings.ALLOWED_HOSTS = request._original_allowed_hosts + del request._original_allowed_hosts del mail.outbox @@ -207,7 +211,6 @@ class override_settings(object): """ def __init__(self, **kwargs): self.options = kwargs - self.wrapped = settings._wrapped def __enter__(self): self.enable() @@ -246,6 +249,7 @@ class override_settings(object): override = UserSettingsHolder(settings._wrapped) for key, new_value in self.options.items(): setattr(override, key, new_value) + self.wrapped = settings._wrapped settings._wrapped = override for key, new_value in self.options.items(): setting_changed.send(sender=settings._wrapped.__class__, @@ -253,6 +257,7 @@ class override_settings(object): def disable(self): settings._wrapped = self.wrapped + del self.wrapped for key in self.options: new_value = getattr(settings, key, None) setting_changed.send(sender=settings._wrapped.__class__, diff --git a/django/utils/html.py b/django/utils/html.py index 0d28c77a61..4893b6b18a 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import re -import string try: from urllib.parse import quote, urlsplit, urlunsplit except ImportError: # Python 2 diff --git a/django/utils/http.py b/django/utils/http.py index f4911b4ec0..4647d89847 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import base64 import calendar import datetime import re @@ -11,7 +12,7 @@ except ImportError: # Python 2 import urlparse urllib_parse.urlparse = urlparse.urlparse - +from binascii import Error as BinasciiError from email.utils import formatdate from django.utils.datastructures import MultiValueDict @@ -202,6 +203,24 @@ def int_to_base36(i): factor -= 1 return ''.join(base36) +def urlsafe_base64_encode(s): + """ + Encodes a bytestring in base64 for use in URLs, stripping any trailing + equal signs. + """ + return base64.urlsafe_b64encode(s).rstrip(b'\n=') + +def urlsafe_base64_decode(s): + """ + Decodes a base64 encoded string, adding back any trailing equal signs that + might have been stripped. + """ + s = s.encode('utf-8') # base64encode should only return ASCII. + try: + return base64.urlsafe_b64decode(s.ljust(len(s) + len(s) % 4, b'=')) + except (LookupError, BinasciiError) as e: + raise ValueError(e) + def parse_etags(etag_str): """ Parses a string with one or several etags passed in If-None-Match and diff --git a/django/utils/text.py b/django/utils/text.py index 4fe92ab509..7bb8e7eb3e 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import re import unicodedata -import warnings from gzip import GzipFile from io import BytesIO diff --git a/django/views/debug.py b/django/views/debug.py index 0458580221..2129a83d67 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -7,7 +7,6 @@ import sys import types from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.http import (HttpResponse, HttpResponseServerError, HttpResponseNotFound, HttpRequest, build_request_repr) from django.template import Template, Context, TemplateDoesNotExist diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index a6bd7d8526..1ebed2a6a1 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -1,5 +1,3 @@ -import warnings - from django.middleware.csrf import CsrfViewMiddleware, get_token from django.utils.decorators import decorator_from_middleware, available_attrs from functools import wraps diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index b31d7a218f..fccacf0bd3 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -17,6 +17,7 @@ class FormMixin(ContextMixin): initial = {} form_class = None success_url = None + prefix = None def get_initial(self): """ @@ -24,6 +25,12 @@ class FormMixin(ContextMixin): """ return self.initial.copy() + def get_prefix(self): + """ + Returns the prefix to use for forms on this view + """ + return self.prefix + def get_form_class(self): """ Returns the form class to use in this view @@ -40,7 +47,11 @@ class FormMixin(ContextMixin): """ Returns the keyword arguments for instantiating the form. """ - kwargs = {'initial': self.get_initial()} + kwargs = { + 'initial': self.get_initial(), + 'prefix': self.get_prefix(), + } + if self.request.method in ('POST', 'PUT'): kwargs.update({ 'data': self.request.POST, @@ -78,6 +89,7 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): """ A mixin that provides a way to show and handle a modelform in a request. """ + fields = None def get_form_class(self): """ @@ -98,13 +110,12 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): # from that model = self.get_queryset().model - fields = getattr(self, 'fields', None) - if fields is None: + if self.fields is None: warnings.warn("Using ModelFormMixin (base class of %s) without " "the 'fields' attribute is deprecated." % self.__class__.__name__, PendingDeprecationWarning) - return model_forms.modelform_factory(model, fields=fields) + return model_forms.modelform_factory(model, fields=self.fields) def get_form_kwargs(self): """ |
