diff --git a/AUTHORS b/AUTHORS index 3c0cb7a7b5..ba1f4036e9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -97,6 +97,7 @@ answer newbie questions, and generally made Django that much better: Ned Batchelder batiste@dosimple.ch Batman + Oliver Beattie Brian Beck Shannon -jj Behrens Esdras Beleza @@ -151,6 +152,7 @@ answer newbie questions, and generally made Django that much better: Antonis Christofides Michal Chruszcz Can Burak Çilingir + Andrew Clark Ian Clelland Travis Cline Russell Cloran @@ -248,6 +250,7 @@ answer newbie questions, and generally made Django that much better: martin.glueck@gmail.com Ben Godfrey GomoX + Gil Gonçalves Guilherme Mesquita Gondim Mario Gonzalez David Gouldin @@ -271,6 +274,7 @@ answer newbie questions, and generally made Django that much better: Brian Harring Brant Harris Ronny Haryanto + Axel Haustant Hawkeye Kent Hauser Joe Heck @@ -486,7 +490,7 @@ answer newbie questions, and generally made Django that much better: Brian Ray Lee Reilly Łukasz Rekucki - remco@diji.biz + Remco Wendt Marc Remolt Bruno Renié David Reynolds @@ -608,6 +612,7 @@ answer newbie questions, and generally made Django that much better: Filip Wasilewski Dan Watson Joel Watts + Russ Webber Lakin Wecker Chris Wesseling Benjamin Wohlwend 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
    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( - '' - '' % \ - # 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 @@ + + + + + + + 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 @@ {% if show_result_count %} - {% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}) + {% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} ({% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}) {% endif %} {% for pair in cl.params.items %} {% ifnotequal pair.0 search_var %}{% 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}>{4}', 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[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + (r'^reset/custom/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', dict(post_reset_redirect='/custom/')), - (r'^reset/custom/named/(?P[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + (r'^reset/custom/named/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', dict(post_reset_redirect='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[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm_uidb36'), + url(r'^reset/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm', 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 similarity index 100% rename from django/core/compat_checks/__init__.py rename to django/core/checks/__init__.py diff --git a/tests/compat_checks/__init__.py b/django/core/checks/compatibility/__init__.py similarity index 100% rename from tests/compat_checks/__init__.py rename to django/core/checks/compatibility/__init__.py diff --git a/django/core/compat_checks/base.py b/django/core/checks/compatibility/base.py similarity index 94% rename from django/core/compat_checks/base.py rename to 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 similarity index 94% rename from django/core/compat_checks/django_1_6_0.py rename to 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 similarity index 83% rename from django/core/management/commands/checksetup.py rename to 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 "" % (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 "" % 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): """ diff --git a/docs/conf.py b/docs/conf.py index feff99b6f4..0c35e935e6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,7 +55,7 @@ copyright = 'Django Software Foundation and contributors' # built documents. # # The short X.Y version. -version = '1.6' +version = '1.7' # The full version, including alpha/beta/rc tags. try: from django import VERSION, get_version @@ -71,7 +71,7 @@ else: release = django_release() # The "development version" of Django -django_next_version = '1.6' +django_next_version = '1.7' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/faq/install.txt b/docs/faq/install.txt index 5a4cab94cf..d221f93d02 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -16,9 +16,8 @@ How do I get started? What are Django's prerequisites? -------------------------------- -Django requires Python, specifically Python 2.6.5 - 2.7.x. No other Python -libraries are required for basic Django usage. Django 1.5 also has -experimental support for Python 3.2.3 and above. +Django requires Python, specifically Python 2.6.5 - 2.7.x, or 3.2.3 and above. +No other Python libraries are required for basic Django usage. For a development environment -- if you just want to experiment with Django -- you don't need to have a separate Web server installed; Django comes with its @@ -43,7 +42,7 @@ Do I lose anything by using Python 2.6 versus newer Python versions, such as Pyt ---------------------------------------------------------------------------------------- Not in the core framework. Currently, Django itself officially supports -Python 2.6 (2.6.5 or higher) and 2.7. However, newer versions of +Python 2.6 (2.6.5 or higher), 2.7, 3.2.3 or higher. However, newer versions of Python are often faster, have more features, and are better supported. If you use a newer version of Python you will also have access to some APIs that aren't available under older versions of Python. @@ -51,12 +50,9 @@ aren't available under older versions of Python. Third-party applications for use with Django are, of course, free to set their own version requirements. -All else being equal, we recommend that you use the latest 2.x release -(currently Python 2.7). This will let you take advantage of the numerous -improvements and optimizations to the Python language since version 2.6. - -Generally speaking, we don't recommend running Django on Python 3 yet; see -below for more. +All else being equal, we recommend that you use the latest 2.7 or 3.x release. +This will let you take advantage of the numerous improvements and optimizations +to the Python language since version 2.6. What Python version can I use with Django? ------------------------------------------ @@ -77,15 +73,12 @@ Django version Python versions Can I use Django with Python 3? ------------------------------- -Django 1.5 introduces experimental support for Python 3.2.3 and above. However, -we don't yet suggest that you use Django and Python 3 in production. +Yes, you can! -Python 3 support should be considered a "preview". It's offered to bootstrap -the transition of the Django ecosystem to Python 3, and to help you start -porting your apps for future Python 3 compatibility. But we're not yet -confident enough to promise stability in production. +Django 1.5 introduced experimental support for Python 3.2.3 and above. -Our current plan is to make Django 1.6 suitable for general use with Python 3. +As of Django 1.6, Python 3 support is considered stable and you can safely use +it in production. See also :doc:`/topics/python3`. Will Django run under shared hosting (like TextDrive or Dreamhost)? ------------------------------------------------------------------- diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 8993872cff..54913a887a 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -31,7 +31,7 @@ Our example object Creating custom fields requires a bit of attention to detail. To make things easier to follow, we'll use a consistent example throughout this document: wrapping a Python object representing the deal of cards in a hand of Bridge_. -Don't worry, you don't have know how to play Bridge to follow this example. +Don't worry, you don't have to know how to play Bridge to follow this example. You only need to know that 52 cards are dealt out equally to four players, who are traditionally called *north*, *east*, *south* and *west*. Our class looks something like this:: diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt index 7749192358..2cbcd8ce7e 100644 --- a/docs/howto/deployment/wsgi/modwsgi.txt +++ b/docs/howto/deployment/wsgi/modwsgi.txt @@ -25,7 +25,8 @@ Basic configuration =================== Once you've got mod_wsgi installed and activated, edit your Apache server's -``httpd.conf`` file and add +``httpd.conf`` file and add the following. If you are using a version of Apache +older than 2.4, replace ``Require all granted`` with ``Allow from all``. .. code-block:: apache @@ -35,7 +36,7 @@ Once you've got mod_wsgi installed and activated, edit your Apache server's Order deny,allow - Allow from all + Require all granted diff --git a/docs/howto/static-files/index.txt b/docs/howto/static-files/index.txt index 3668c5dc41..db8bd38e9c 100644 --- a/docs/howto/static-files/index.txt +++ b/docs/howto/static-files/index.txt @@ -68,7 +68,7 @@ details on how ``staticfiles`` finds your files. Now we *might* be able to get away with putting our static files directly in ``my_app/static/`` (rather than creating another ``my_app`` subdirectory), but it would actually be a bad idea. Django will use the - last static file it finds whose name matches, and if you had a static file + first static file it finds whose name matches, and if you had a static file with the same name in a *different* application, Django would be unable to distinguish between them. We need to be able to point Django at the right one, and the easiest way to ensure this is by *namespacing* them. That is, diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 43b799ed51..7bb59bc329 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -255,7 +255,11 @@ Keywords ~~~~~~~~ With this field you may label a ticket with multiple keywords. This can be -useful, for example, to group several tickets of a same theme. +useful, for example, to group several tickets of a same theme. Keywords can +either be comma or space separated. Keyword search finds the keyword string +anywhere in the keywords. For example, clicking on a ticket with the keyword +"form" will yield similar tickets tagged with keywords containing strings such +as "formset", "modelformset", and "ManagementForm". .. _closing-tickets: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 45f82b49e6..9672746717 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -326,6 +326,14 @@ these changes. remove calls to this method, and instead ensure that their auth related views are CSRF protected, which ensures that cookies are enabled. +* The version of :func:`django.contrib.auth.views.password_reset_confirm` that + supports base36 encoded user IDs + (``django.contrib.auth.views.password_reset_confirm_uidb36``) will be + removed. If your site has been running Django 1.6 for more than + :setting:`PASSWORD_RESET_TIMEOUT_DAYS`, this change will have no effect. If + not, then any password reset links generated before you upgrade to Django 1.7 + won't work after the upgrade. + 1.8 --- diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt index 4247b45238..879cda913a 100644 --- a/docs/intro/reusable-apps.txt +++ b/docs/intro/reusable-apps.txt @@ -67,7 +67,7 @@ After the previous tutorials, our project should look like this:: admin.py models.py static/ - polls + polls/ images/ background.gif style.css diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 6193ec45f7..91409848cf 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -339,14 +339,14 @@ Put the following code in that template: Now let's update our ``index`` view in ``polls/views.py`` to use the template:: from django.http import HttpResponse - from django.template import Context, loader + from django.template import RequestContext, loader from polls.models import Poll def index(request): latest_poll_list = Poll.objects.order_by('-pub_date')[:5] template = loader.get_template('polls/index.html') - context = Context({ + context = RequestContext(request, { 'latest_poll_list': latest_poll_list, }) return HttpResponse(template.render(context)) @@ -377,7 +377,7 @@ rewritten:: return render(request, 'polls/index.html', context) Note that once we've done this in all these views, we no longer need to import -:mod:`~django.template.loader`, :class:`~django.template.Context` and +:mod:`~django.template.loader`, :class:`~django.template.RequestContext` and :class:`~django.http.HttpResponse` (you'll want to keep ``HttpResponse`` if you still have the stub methods for ``detail``, ``results``, and ``vote``). diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt index df00f87aa0..272c852181 100644 --- a/docs/ref/class-based-views/flattened-index.txt +++ b/docs/ref/class-based-views/flattened-index.txt @@ -151,6 +151,7 @@ FormView * :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] +* :attr:`~django.views.generic.edit.FormMixin.prefix` [:meth:`~django.views.generic.edit.FormMixin.get_prefix`] * :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` [:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`] * :attr:`~django.views.generic.edit.FormMixin.success_url` [:meth:`~django.views.generic.edit.FormMixin.get_success_url`] * :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` [:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`] @@ -177,11 +178,13 @@ CreateView * :attr:`~django.views.generic.base.TemplateResponseMixin.content_type` * :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.edit.ModelFormMixin.fields` * :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] * :attr:`~django.views.generic.detail.SingleObjectMixin.model` * :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.edit.FormMixin.prefix` [:meth:`~django.views.generic.edit.FormMixin.get_prefix`] * :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] * :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` [:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`] * :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] @@ -216,11 +219,13 @@ UpdateView * :attr:`~django.views.generic.base.TemplateResponseMixin.content_type` * :attr:`~django.views.generic.detail.SingleObjectMixin.context_object_name` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name`] +* :attr:`~django.views.generic.edit.ModelFormMixin.fields` * :attr:`~django.views.generic.edit.FormMixin.form_class` [:meth:`~django.views.generic.edit.FormMixin.get_form_class`] * :attr:`~django.views.generic.base.View.http_method_names` * :attr:`~django.views.generic.edit.FormMixin.initial` [:meth:`~django.views.generic.edit.FormMixin.get_initial`] * :attr:`~django.views.generic.detail.SingleObjectMixin.model` * :attr:`~django.views.generic.detail.SingleObjectMixin.pk_url_kwarg` +* :attr:`~django.views.generic.edit.FormMixin.prefix` [:meth:`~django.views.generic.edit.FormMixin.get_prefix`] * :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`] * :attr:`~django.views.generic.base.TemplateResponseMixin.response_class` [:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response`] * :attr:`~django.views.generic.detail.SingleObjectMixin.slug_field` [:meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field`] diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 48d363b3b2..bf1c10df13 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -35,6 +35,12 @@ FormMixin The URL to redirect to when the form is successfully processed. + .. attribute:: prefix + + .. versionadded:: 1.6 + + The :attr:`~django.forms.Form.prefix` for the generated form. + .. method:: get_initial() Retrieve initial data for the form. By default, returns a copy of @@ -58,6 +64,13 @@ FormMixin request is a ``POST`` or ``PUT``, the request data (``request.POST`` and ``request.FILES``) will also be provided. + .. method:: get_prefix() + + .. versionadded:: 1.6 + + Determine the :attr:`~django.forms.Form.prefix` for the generated form. + Returns :attr:`~django.views.generic.edit.FormMixin.prefix` by default. + .. method:: get_success_url() Determine the URL to redirect to when the form is successfully diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 90ef68837a..318ce297a2 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -2278,9 +2278,14 @@ your URLconf. Specifically, add these four patterns: url(r'^admin/password_reset/$', 'django.contrib.auth.views.password_reset', name='admin_password_reset'), url(r'^admin/password_reset/done/$', 'django.contrib.auth.views.password_reset_done', name='password_reset_done'), - url(r'^reset/(?P[0-9A-Za-z]+)-(?P.+)/$', 'django.contrib.auth.views.password_reset_confirm', name='password_reset_confirm'), + url(r'^reset/(?P[0-9A-Za-z_\-]+)/(?P.+)/$', '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'), +.. versionchanged:: 1.6 + + The pattern for :func:`~django.contrib.auth.views.password_reset_confirm` + changed as the ``uid`` is now base 64 encoded. + (This assumes you've added the admin at ``admin/`` and requires that you put the URLs starting with ``^admin/`` before the line that includes the admin app itself). diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 56a15cb9e0..4467ed3a6e 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -358,7 +358,7 @@ with a caching decorator -- you must name your sitemap view and pass from django.views.decorators.cache import cache_page urlpatterns = patterns('', - url(r'^sitemap.xml$', + url(r'^sitemap\.xml$', cache_page(86400)(sitemaps_views.index), {'sitemaps': sitemaps, 'sitemap_url_name': 'sitemaps'}), url(r'^sitemap-(?P
    .+)\.xml$', diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index a648ac1709..4e5f136e2e 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -623,6 +623,14 @@ If you're getting this error, you can solve it by: SQLite does not support the ``SELECT ... FOR UPDATE`` syntax. Calling it will have no effect. +"pyformat" parameter style in raw queries not supported +------------------------------------------------------- + +For most backends, raw queries (``Manager.raw()`` or ``cursor.execute()``) +can use the "pyformat" parameter style, where placeholders in the query +are given as ``'%(name)s'`` and the parameters are passed as a dictionary +rather than a list. SQLite does not support this. + .. _sqlite-connection-queries: Parameters not quoted in ``connection.queries`` diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 8146dfd341..f5c1058b17 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1083,6 +1083,22 @@ define the details of how the relation works. user = models.ForeignKey(User, related_name='+') +.. attribute:: ForeignKey.related_query_name + + .. versionadded:: 1.6 + + The name to use for the reverse filter name from the target model. + Defaults to the value of :attr:`related_name` if it is set, otherwise it + defaults to the name of the model:: + + # Declare the ForeignKey with related_query_name + class Tag(models.Model): + article = models.ForeignKey(Article, related_name="tags", related_query_name="tag") + name = models.CharField(max_length=255) + + # That's now the name of the reverse filter + article_instance.filter(tag__name="important") + .. attribute:: ForeignKey.to_field The field on the related object that the relation is to. By default, Django @@ -1207,6 +1223,12 @@ that control how the relationship functions. users = models.ManyToManyField(User, related_name='u+') referents = models.ManyToManyField(User, related_name='ref+') +.. attribute:: ForeignKey.related_query_name + + .. versionadded:: 1.6 + + Same as :attr:`ForeignKey.related_query_name`. + .. attribute:: ManyToManyField.limit_choices_to Same as :attr:`ForeignKey.limit_choices_to`. diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index cfc95db092..17c9aa9fb7 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -679,8 +679,11 @@ For every :class:`~django.db.models.DateField` and returns the next and previous object with respect to the date field, raising a :exc:`~django.core.exceptions.DoesNotExist` exception when appropriate. -Both methods accept optional keyword arguments, which should be in the format -described in :ref:`Field lookups `. +Both of these methods will perform their queries using the default +manager for the model. If you need to emulate filtering used by a +custom manager, or want to perform one-off custom filtering, both +methods also accept optional keyword arguments, which should be in the +format described in :ref:`Field lookups `. Note that in the case of identical date values, these methods will use the primary key as a tie-breaker. This guarantees that no records are skipped or diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 897af275a0..215931768c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1350,8 +1350,14 @@ A data structure containing configuration information. The contents of this data structure will be passed as the argument to the configuration method described in :setting:`LOGGING_CONFIG`. -The default logging configuration passes HTTP 500 server errors to an -email log handler; all other log messages are given to a NullHandler. +Among other things, the default logging configuration passes HTTP 500 server +errors to an email log handler when :setting:`DEBUG` is ``False``. See also +:ref:`configuring-logging`. + +You can see the default logging configuration by looking in +``django/utils/log.py`` (or view the `online source`__). + +__ https://github.com/django/django/blob/master/django/utils/log.py .. setting:: LOGGING_CONFIG @@ -2564,7 +2570,9 @@ various locations. The default will find files stored in the :setting:`STATICFILES_DIRS` setting (using ``django.contrib.staticfiles.finders.FileSystemFinder``) and in a ``static`` subdirectory of each app (using -``django.contrib.staticfiles.finders.AppDirectoriesFinder``) +``django.contrib.staticfiles.finders.AppDirectoriesFinder``). If multiple +files with the same name are present, the first file that is found will be +used. One finder is disabled by default: ``django.contrib.staticfiles.finders.DefaultStorageFinder``. If added to diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 06ba2cb3e8..0253832b8d 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -255,7 +255,7 @@ Arguments sent with this signal: ``pk_set`` For the ``pre_add``, ``post_add``, ``pre_remove`` and ``post_remove`` - actions, this is a list of primary key values that have been added to + actions, this is a set of primary key values that have been added to or removed from the relation. For the ``pre_clear`` and ``post_clear`` actions, this is ``None``. @@ -284,7 +284,7 @@ If we connected a handler like this:: and then did something like this:: - >>> p = Pizza.object.create(...) + >>> p = Pizza.objects.create(...) >>> t = Topping.objects.create(...) >>> p.toppings.add(t) @@ -307,7 +307,7 @@ Argument Value ``model`` ``Topping`` (the class of the objects added to the ``Pizza``) -``pk_set`` ``[t.id]`` (since only ``Topping t`` was added to the relation) +``pk_set`` ``set([t.id])`` (since only ``Topping t`` was added to the relation) ``using`` ``"default"`` (since the default router sends writes here) ============== ============================================================ @@ -334,7 +334,7 @@ Argument Value ``model`` ``Pizza`` (the class of the objects removed from the ``Topping``) -``pk_set`` ``[p.id]`` (since only ``Pizza p`` was removed from the +``pk_set`` ``set([p.id])`` (since only ``Pizza p`` was removed from the relation) ``using`` ``"default"`` (since the default router sends writes here) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 45d7781403..8d722829fb 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -649,6 +649,20 @@ escaping HTML. Converts a positive integer to a base 36 string. On Python 2 ``i`` must be smaller than :data:`sys.maxint`. +.. function:: urlsafe_base64_encode(s) + + .. versionadded:: 1.6 + + Encodes a bytestring in base64 for use in URLs, stripping any trailing + equal signs. + +.. function:: urlsafe_base64_decode(s) + + .. versionadded:: 1.6 + + Decodes a base64 encoded string, adding back any trailing equal signs that + might have been stripped. + ``django.utils.module_loading`` =============================== diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index bd6255eae6..1fd98e1271 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -121,10 +121,10 @@ GeoDjango now provides :ref:`form fields and widgets ` for its geo-specialized fields. They are OpenLayers-based by default, but they can be customized to use any other JS framework. -``checksetup`` management command added for verifying compatibility -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``check`` management command added for verifying compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A ``checksetup`` management command was added, enabling you to verify if your +A ``check`` management command was added, enabling you to verify if your current configuration (currently oriented at settings) is compatible with the current version of Django. @@ -330,6 +330,19 @@ Minor features behavior of clearing filters by setting the :attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``. +* Added + :meth:`FormMixin.get_prefix` + (which returns + :attr:`FormMixin.prefix` by + default) to allow customizing the :attr:`~django.forms.Form.prefix` of the + form. + +* Raw queries (``Manager.raw()`` or ``cursor.execute()``) can now use the + "pyformat" parameter style, where placeholders in the query are given as + ``'%(name)s'`` and the parameters are passed as a dictionary rather than + a list (except on SQLite). This has long been possible (but not officially + supported) on MySQL and PostgreSQL, and is now also available on Oracle. + Backwards incompatible changes in 1.6 ===================================== @@ -649,6 +662,59 @@ rely on the previous URLs. If you want to revert to the original behavior you can set the :attr:`~django.contrib.admin.ModelAdmin.preserve_filters` attribute to ``False``. +``django.contrib.auth`` password reset uses base 64 encoding of ``User`` PK +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Past versions of Django used base 36 encoding of the ``User`` primary key in +the password reset views and URLs +(:func:`django.contrib.auth.views.password_reset_confirm`). Base 36 encoding is +sufficient if the user primary key is an integer, however, with the +introduction of custom user models in Django 1.5, that assumption may no longer +be true. + +:func:`django.contrib.auth.views.password_reset_confirm` has been modified to +take a ``uidb64`` parameter instead of ``uidb36``. If you are reversing this +view, for example in a custom ``password_reset_email.html`` template, be sure +to update your code. + +A temporary shim for :func:`django.contrib.auth.views.password_reset_confirm` +that will allow password reset links generated prior to Django 1.6 to continue +to work has been added to provide backwards compatibility; this will be removed +in Django 1.7. Thus, as long as your site has been running Django 1.6 for more +than :setting:`PASSWORD_RESET_TIMEOUT_DAYS`, this change will have no effect. +If not (for example, if you upgrade directly from Django 1.5 to Django 1.7), +then any password reset links generated before you upgrade to Django 1.7 or +later won't work after the upgrade. + +In addition, if you have any custom password reset URLs, you will need to +update them by replacing ``uidb36`` with ``uidb64`` and the dash that follows +that pattern with a slash. Also add ``_\-`` to the list of characters that may +match the ``uidb64`` pattern. + +For example:: + + url(r'^reset/(?P[0-9A-Za-z]+)-(?P.+)/$', + 'django.contrib.auth.views.password_reset_confirm', + name='password_reset_confirm'), + +becomes:: + + url(r'^reset/(?P[0-9A-Za-z_\-]+)/(?P.+)/$', + 'django.contrib.auth.views.password_reset_confirm', + name='password_reset_confirm'), + +You may also want to add the shim to support the old style reset links. Using +the example above, you would modify the existing url by replacing +``django.contrib.auth.views.password_reset_confirm`` with +``django.contrib.auth.views.password_reset_confirm_uidb36`` and also remove +the ``name`` argument so it doesn't conflict with the new url:: + + url(r'^reset/(?P[0-9A-Za-z]+)-(?P.+)/$', + 'django.contrib.auth.views.password_reset_confirm_uidb36'), + +You can remove this url pattern after your app has been deployed with Django +1.6 for :setting:`PASSWORD_RESET_TIMEOUT_DAYS`. + Miscellaneous ~~~~~~~~~~~~~ @@ -725,6 +791,12 @@ Miscellaneous returned ``False`` for blank passwords. This has been corrected in this release: blank passwords are now valid. +* The admin :attr:`~django.contrib.admin.ModelAdmin.changelist_view` previously + accepted a ``pop`` GET parameter to signify it was to be displayed in a popup. + This parameter has been renamed to ``_popup`` to be consistent with the rest + of the admin views. You should update your custom templates if they use the + previous parameter name. + Features deprecated in 1.6 ========================== @@ -842,6 +914,13 @@ on a widget, you should now define this method on the form field itself. ``Model._meta.module_name`` was renamed to ``model_name``. Despite being a private API, it will go through a regular deprecation path. +``get_(add|change|delete)_permission`` model _meta methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Model._meta.get_(add|change|delete)_permission`` methods were deprecated. +Even if they were not part of the public API they'll also go through +a regular deprecation path. + ``get_query_set`` and similar methods renamed to ``get_queryset`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 8849520b11..e2fa0c287e 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -817,7 +817,7 @@ patterns. * ``protocol``: http or https - * ``uid``: The user's id encoded in base 36. + * ``uid``: The user's primary key encoded in base 64. * ``token``: Token to check that the reset link is valid. @@ -826,7 +826,12 @@ patterns. .. code-block:: html+django Someone asked for password reset for email {{ email }}. Follow the link below: - {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} + {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} + + .. versionchanged:: 1.6 + + Reversing ``password_reset_confirm`` takes a ``uidb64`` argument instead + of ``uidb36``. The same template context is used for subject template. Subject must be single line plain text string. @@ -846,7 +851,7 @@ patterns. Defaults to :file:`registration/password_reset_done.html` if not supplied. -.. function:: password_reset_confirm(request[, uidb36, token, template_name, token_generator, set_password_form, post_reset_redirect]) +.. function:: password_reset_confirm(request[, uidb64, token, template_name, token_generator, set_password_form, post_reset_redirect]) Presents a form for entering a new password. @@ -854,7 +859,12 @@ patterns. **Optional arguments:** - * ``uidb36``: The user's id encoded in base 36. Defaults to ``None``. + * ``uidb64``: The user's id encoded in base 64. Defaults to ``None``. + + .. versionchanged:: 1.6 + + The ``uidb64`` parameter was previously base 36 encoded and named + ``uidb36``. * ``token``: Token to check that the password is valid. Defaults to ``None``. @@ -877,8 +887,8 @@ patterns. * ``form``: The form (see ``set_password_form`` above) for setting the new user's password. - * ``validlink``: Boolean, True if the link (combination of uidb36 and - token) is valid or unused yet. + * ``validlink``: Boolean, True if the link (combination of ``uidb64`` and + ``token``) is valid or unused yet. .. function:: password_reset_complete(request[,template_name]) diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 7ffa471e79..8c2d0db041 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -92,6 +92,15 @@ We'll be using these models:: def __unicode__(self): return self.name + class Author(models.Model): + salutation = models.CharField(max_length=10) + name = models.CharField(max_length=200) + email = models.EmailField() + headshot = models.ImageField(upload_to='author_headshots') + + def __unicode__(self): + return self.name + class Book(models.Model): title = models.CharField(max_length=100) authors = models.ManyToManyField('Author') @@ -132,11 +141,11 @@ bit is just the lowercased version of the model's name. enabled in :setting:`TEMPLATE_LOADERS`, a template location could be: /path/to/project/books/templates/books/publisher_list.html -.. highlightlang:: html+django - This template will be rendered against a context containing a variable called ``object_list`` that contains all the publisher objects. A very simple template -might look like the following:: +might look like the following: + +.. code-block:: html+django {% extends "base.html" %} @@ -159,8 +168,6 @@ consider some of the common ways you might customize and extend generic views. Making "friendly" template contexts ----------------------------------- -.. highlightlang:: python - You might have noticed that our sample publisher list template stores all the publishers in a variable named ``object_list``. While this works just fine, it isn't all that "friendly" to template authors: they have to "just know" that @@ -198,15 +205,12 @@ provided by the generic view. For example, think of showing a list of all the books on each publisher detail page. The :class:`~django.views.generic.detail.DetailView` generic view provides the publisher to the context, but how do we get additional information -in that template. +in that template? -However, there is; you can subclass -:class:`~django.views.generic.detail.DetailView` and provide your own -implementation of the ``get_context_data`` method. The default -implementation of this that comes with -:class:`~django.views.generic.detail.DetailView` simply adds in the -object being displayed to the template, but you can override it to send -more:: +The answer is to subclass :class:`~django.views.generic.detail.DetailView` +and provide your own implementation of the ``get_context_data`` method. +The default implementation simply adds the object being displayed to the +template, but you can override it to send more:: from django.views.generic import DetailView from books.models import Publisher, Book @@ -224,10 +228,10 @@ more:: .. note:: - Generally, get_context_data will merge the context data of all parent + Generally, ``get_context_data`` will merge the context data of all parent classes with those of the current class. To preserve this behavior in your own classes where you want to alter the context, you should be sure to call - get_context_data on the super class. When no two classes try to define the + ``get_context_data`` on the super class. When no two classes try to define the same key, this will give the expected results. However if any class attempts to override a key after parent classes have set it (after the call to super), any children of that class will also need to explicitly set it @@ -372,7 +376,7 @@ Performing extra work The last common pattern we'll look at involves doing some extra work before or after calling the generic view. -Imagine we had a ``last_accessed`` field on our ``Author`` object that we were +Imagine we had a ``last_accessed`` field on our ``Author`` model that we were using to keep track of the last time anybody looked at that author:: # models.py @@ -382,7 +386,7 @@ using to keep track of the last time anybody looked at that author:: salutation = models.CharField(max_length=10) name = models.CharField(max_length=200) email = models.EmailField() - headshot = models.ImageField(upload_to='/tmp') + headshot = models.ImageField(upload_to='author_headshots') last_accessed = models.DateTimeField() The generic ``DetailView`` class, of course, wouldn't know anything about this diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 7c4e02cc4e..f12672df69 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -190,8 +190,8 @@ the foreign key relation to the model:: # ... -In the view, ensure that you exclude ``created_by`` in the list of fields to -edit, and override +In the view, ensure that you don't include ``created_by`` in the list of fields +to edit, and override :meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the user:: # views.py @@ -256,3 +256,4 @@ works for AJAX requests as well as 'normal' form POSTs:: class AuthorCreate(AjaxableResponseMixin, CreateView): model = Author + fields = ['name'] diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt index dbbbea25f0..a65b887921 100644 --- a/docs/topics/class-based-views/intro.txt +++ b/docs/topics/class-based-views/intro.txt @@ -208,7 +208,7 @@ A similar class-based view might look like:: def get(self, request, *args, **kwargs): form = self.form_class(initial=self.initial) - return render(request, self.template_name, {'form': form}) + return render(request, self.template_name, {'form': form}) def post(self, request, *args, **kwargs): form = self.form_class(request.POST) diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 3a4811e7bb..84d7417233 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -34,7 +34,7 @@ interface to working with templates in class-based views. :class:`~django.views.generic.base.TemplateResponseMixin` Every built in view which returns a :class:`~django.template.response.TemplateResponse` will call the - :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` + :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` method that ``TemplateResponseMixin`` provides. Most of the time this will be called for you (for instance, it is called by the ``get()`` method implemented by both :class:`~django.views.generic.base.TemplateView` and @@ -44,7 +44,7 @@ interface to working with templates in class-based views. it. For an example of this, see the :ref:`JSONResponseMixin example `. - ``render_to_response`` itself calls + ``render_to_response()`` itself calls :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names`, which by default will just look up :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` on @@ -60,9 +60,9 @@ interface to working with templates in class-based views. :class:`~django.views.generic.base.ContextMixin` Every built in view which needs context data, such as for rendering a template (including ``TemplateResponseMixin`` above), should call - :meth:`~django.views.generic.base.ContextMixin.get_context_data` passing + :meth:`~django.views.generic.base.ContextMixin.get_context_data()` passing any data they want to ensure is in there as keyword arguments. - ``get_context_data`` returns a dictionary; in ``ContextMixin`` it + ``get_context_data()`` returns a dictionary; in ``ContextMixin`` it simply returns its keyword arguments, but it is common to override this to add more members to the dictionary. @@ -107,7 +107,7 @@ URLConf, and looks the object up either from the on the view, or the :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` attribute if that's provided). ``SingleObjectMixin`` also overrides -:meth:`~django.views.generic.base.ContextMixin.get_context_data`, +:meth:`~django.views.generic.base.ContextMixin.get_context_data()`, which is used across all Django's built in class-based views to supply context data for template renders. @@ -152,7 +152,7 @@ here would be to dynamically vary the objects, such as depending on the current user or to exclude posts in the future for a blog. :class:`~django.views.generic.list.MultipleObjectMixin` also overrides -:meth:`~django.views.generic.base.ContextMixin.get_context_data` to +:meth:`~django.views.generic.base.ContextMixin.get_context_data()` to include appropriate context variables for pagination (providing dummies if pagination is disabled). It relies on ``object_list`` being passed in as a keyword argument, which :class:`ListView` arranges for @@ -286,12 +286,18 @@ One way to do this is to combine :class:`ListView` with for the paginated list of books can hang off the publisher found as the single object. In order to do this, we need to have two different querysets: -**Publisher queryset for use in get_object** - We'll set that up directly when we call ``get_object()``. +``Publisher`` queryset for use in + :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` + We'll set the ``model`` attribute on the view and rely on the default + implementation of ``get_object()`` to fetch the correct ``Publisher`` + object. -**Book queryset for use by ListView** - We'll figure that out ourselves in ``get_queryset()`` so we - can take into account the ``Publisher`` we're looking at. +``Book`` queryset for use by :class:`~django.views.generic.list.ListView` + The default implementation of ``get_queryset()`` uses the ``model`` attribute + to construct the queryset. This conflicts with our use of this attribute + for ``get_object()`` so we'll override that method and have it return + the queryset of ``Book`` objects linked to the ``Publisher`` we're looking + at. .. note:: @@ -300,7 +306,7 @@ object. In order to do this, we need to have two different querysets: :class:`ListView` will put things in the context data under the value of ``context_object_name`` if it's set, we'll instead explictly - ensure the Publisher is in the context data. :class:`ListView` + ensure the ``Publisher`` is in the context data. :class:`ListView` will add in the suitable ``page_obj`` and ``paginator`` for us providing we remember to call ``super()``. @@ -311,31 +317,36 @@ Now we can write a new ``PublisherDetail``:: from books.models import Publisher class PublisherDetail(SingleObjectMixin, ListView): + model = Publisher # for SingleObjectMixin.get_object paginate_by = 2 template_name = "books/publisher_detail.html" + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super(PublisherDetail, self).get(request, *args, **kwargs) + def get_context_data(self, **kwargs): - kwargs['publisher'] = self.object - return super(PublisherDetail, self).get_context_data(**kwargs) + context = super(PublisherDetail, self).get_context_data(**kwargs) + context['publisher'] = self.object + return context def get_queryset(self): - self.object = self.get_object(Publisher.objects.all()) return self.object.book_set.all() -Notice how we set ``self.object`` within ``get_queryset()`` so we -can use it again later in ``get_context_data()``. If you don't set -``template_name``, the template will default to the normal +Notice how we set ``self.object`` within ``get()`` so we +can use it again later in ``get_context_data()`` and ``get_queryset()``. +If you don't set ``template_name``, the template will default to the normal :class:`ListView` choice, which in this case would be ``"books/book_list.html"`` because it's a list of books; :class:`ListView` knows nothing about :class:`~django.views.generic.detail.SingleObjectMixin`, so it doesn't have -any clue this view is anything to do with a Publisher. - -.. highlightlang:: html+django +any clue this view is anything to do with a ``Publisher``. The ``paginate_by`` is deliberately small in the example so you don't have to create lots of books to see the pagination working! Here's the -template you'd want to use:: +template you'd want to use: + +.. code-block: html+django {% extends "base.html" %} @@ -427,8 +438,6 @@ code so that on ``POST`` the form gets called appropriately. both of the views implement ``get()``, and things would get much more confusing. -.. highlightlang:: python - Our new ``AuthorDetail`` looks like this:: # CAUTION: you almost certainly do not want to do this. @@ -451,21 +460,18 @@ Our new ``AuthorDetail`` looks like this:: form_class = AuthorInterestForm def get_success_url(self): - return reverse( - 'author-detail', - kwargs = {'pk': self.object.pk}, - ) + return reverse('author-detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): + context = super(AuthorDetail, self).get_context_data(**kwargs) form_class = self.get_form_class() - form = self.get_form(form_class) - context = { - 'form': form - } - context.update(kwargs) - return super(AuthorDetail, self).get_context_data(**context) + context['form'] = self.get_form(form_class) + return context def post(self, request, *args, **kwargs): + if not request.user.is_authenticated(): + return HttpResponseForbidden() + self.object = self.get_object() form_class = self.get_form_class() form = self.get_form(form_class) if form.is_valid(): @@ -474,10 +480,8 @@ Our new ``AuthorDetail`` looks like this:: return self.form_invalid(form) def form_valid(self, form): - if not self.request.user.is_authenticated(): - return HttpResponseForbidden() - self.object = self.get_object() - # record the interest using the message in form.cleaned_data + # Here, we would record the user's interest using the message + # passed in form.cleaned_data['message'] return super(AuthorDetail, self).form_valid(form) ``get_success_url()`` is just providing somewhere to redirect to, @@ -530,15 +534,12 @@ write our own ``get_context_data()`` to make the message = forms.CharField() class AuthorDisplay(DetailView): - - queryset = Author.objects.all() + model = Author def get_context_data(self, **kwargs): - context = { - 'form': AuthorInterestForm(), - } - context.update(kwargs) - return super(AuthorDisplay, self).get_context_data(**context) + context = super(AuthorDisplay, self).get_context_data(**kwargs) + context['form'] = AuthorInterestForm() + return context Then the ``AuthorInterest`` is a simple :class:`FormView`, but we have to bring in :class:`~django.views.generic.detail.SingleObjectMixin` so we @@ -558,24 +559,14 @@ template as ``AuthorDisplay`` is using on ``GET``. form_class = AuthorInterestForm model = Author - def get_context_data(self, **kwargs): - context = { - 'object': self.get_object(), - } - return super(AuthorInterest, self).get_context_data(**context) - - def get_success_url(self): - return reverse( - 'author-detail', - kwargs = {'pk': self.object.pk}, - ) - - def form_valid(self, form): - if not self.request.user.is_authenticated(): + def post(self, request, *args, **kwargs): + if not request.user.is_authenticated(): return HttpResponseForbidden() self.object = self.get_object() - # record the interest using the message in form.cleaned_data - return super(AuthorInterest, self).form_valid(form) + return super(AuthorInterest, self).post(request, *args, **kwargs) + + def get_success_url(self): + return reverse('author-detail', kwargs={'pk': self.object.pk}) Finally we bring this together in a new ``AuthorDetail`` view. We already know that calling :meth:`~django.views.generic.base.View.as_view()` on @@ -627,15 +618,13 @@ For example, a simple JSON mixin might look something like this:: """ A mixin that can be used to render a JSON response. """ - response_class = HttpResponse - - def render_to_response(self, context, **response_kwargs): + def render_to_json_response(self, context, **response_kwargs): """ Returns a JSON response, transforming 'context' to make the payload. """ - response_kwargs['content_type'] = 'application/json' - return self.response_class( + return HttpResponse( self.convert_context_to_json(context), + content_type='application/json', **response_kwargs ) @@ -647,12 +636,22 @@ For example, a simple JSON mixin might look something like this:: # -- can be serialized as JSON. return json.dumps(context) -Now we mix this into the base TemplateView:: +.. note:: + + Check out the :doc:`/topics/serialization` documentation for more + information on how to correctly transform Django models and querysets into + JSON. + +This mixin provides a ``render_to_json_response()`` method with the same signature +as :func:`~django.views.generic.base.TemplateResponseMixin.render_to_response()`. +To use it, we simply need to mix it into a ``TemplateView`` for example, +and override ``render_to_response()`` to call ``render_to_json_response()`` instead:: from django.views.generic import TemplateView class JSONView(JSONResponseMixin, TemplateView): - pass + def render_to_response(self, context, **response_kwargs): + return self.render_to_json_response(context, **response_kwargs) Equally we could use our mixin with one of the generic views. We can make our own version of :class:`~django.views.generic.detail.DetailView` by mixing @@ -664,7 +663,8 @@ rendering behavior has been mixed in):: from django.views.generic.detail import BaseDetailView class JSONDetailView(JSONResponseMixin, BaseDetailView): - pass + def render_to_response(self, context, **response_kwargs): + return self.render_to_json_response(context, **response_kwargs) This view can then be deployed in the same way as any other :class:`~django.views.generic.detail.DetailView`, with exactly the @@ -678,20 +678,21 @@ in both the ``JSONResponseMixin`` and a :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, and override the implementation of :func:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` -to defer to the appropriate subclass depending on the type of response that the -user requested:: +to defer to the appropriate rendering method depending on the type of response +that the user requested:: from django.views.generic.detail import SingleObjectTemplateResponseMixin class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): # Look for a 'format=json' GET argument - if self.request.GET.get('format','html') == 'json': - return JSONResponseMixin.render_to_response(self, context) + if self.request.GET.get('format') == 'json': + return self.render_to_json_response(context) else: - return SingleObjectTemplateResponseMixin.render_to_response(self, context) + return super(HybridDetailView, self).render_to_response(context) -Because of the way that Python resolves method overloading, the local -``render_to_response()`` implementation will override the versions provided by -``JSONResponseMixin`` and -:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. +Because of the way that Python resolves method overloading, the call to +``super(HybridDetailView, self).render_to_response(context)`` ends up +calling the +:meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` +implementation of :class:`~django.views.generic.base.TemplateResponseMixin`. diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 2ec31a4988..7437d51d28 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -166,9 +166,17 @@ argument to ``raw()``:: >>> lname = 'Doe' >>> Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [lname]) -``params`` is a list of parameters. You'll use ``%s`` placeholders in the -query string (regardless of your database engine); they'll be replaced with -parameters from the ``params`` list. +``params`` is a list or dictionary of parameters. You'll use ``%s`` +placeholders in the query string for a list, or ``%(key)s`` +placeholders for a dictionary (where ``key`` is replaced by a +dictionary key, of course), regardless of your database engine. Such +placeholders will be replaced with parameters from the ``params`` +argument. + +.. note:: Dictionary params not supported with SQLite + + Dictionary params are not supported with the SQLite backend; with + this backend, you must pass parameters as a list. .. warning:: @@ -181,14 +189,21 @@ parameters from the ``params`` list. **Don't.** - Using the ``params`` list completely protects you from `SQL injection + Using the ``params`` argument completely protects you from `SQL injection attacks`__, a common exploit where attackers inject arbitrary SQL into your database. If you use string interpolation, sooner or later you'll fall victim to SQL injection. As long as you remember to always use the - ``params`` list you'll be protected. + ``params`` argument you'll be protected. __ http://en.wikipedia.org/wiki/SQL_injection +.. versionchanged:: 1.6 + + In Django 1.5 and earlier, you could pass parameters as dictionaries + when using PostgreSQL or MySQL, although this wasn't documented. Now + you can also do this whem using Oracle, and it is officially supported. + + .. _executing-custom-sql: Executing custom SQL directly diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index e9a626f56b..903579cc38 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -389,6 +389,27 @@ The following example demonstrates the use of savepoints:: transaction.savepoint_rollback(sid) # open transaction now contains only a.save() +.. versionadded:: 1.6 + +Savepoints may be used to recover from a database error by performing a partial +rollback. If you're doing this inside an :func:`atomic` block, the entire block +will still be rolled back, because it doesn't know you've handled the situation +at a lower level! To prevent this, you can control the rollback behavior with +the following functions. + +.. function:: get_rollback(using=None) + +.. function:: set_rollback(rollback, using=None) + +Setting the rollback flag to ``True`` forces a rollback when exiting the +innermost atomic block. This may be useful to trigger a rollback without +raising an exception. + +Setting it to ``False`` prevents such a rollback. Before doing that, make sure +you've rolled back the transaction to a known-good savepoint within the current +atomic block! Otherwise you're breaking atomicity and data corruption may +occur. + Database-specific notes ======================= diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 54d748d961..f6fa27e27c 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -371,8 +371,8 @@ Custom file upload handlers **must** define the following methods: ``receive_data_chunk`` methods. In this way, one handler can be a "filter" for other handlers. - Return ``None`` from ``receive_data_chunk`` to sort-circuit remaining - upload handlers from getting this chunk.. This is useful if you're + Return ``None`` from ``receive_data_chunk`` to short-circuit remaining + upload handlers from getting this chunk. This is useful if you're storing the uploaded data yourself and don't want future handlers to store a copy of the data. diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index 52a2935977..5c8725172a 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -277,8 +277,8 @@ will be raised if more than one object is found. .. function:: get_list_or_404(klass, *args, **kwargs) Returns the result of :meth:`~django.db.models.query.QuerySet.filter()` on a - given model manager, raising :class:`~django.http.Http404` if the resulting - list is empty. + given model manager cast to a list, raising :class:`~django.http.Http404` if + the resulting list is empty. Required arguments ------------------ diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 8268051a36..d56b1be20f 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -221,10 +221,12 @@ Order in which tests are executed In order to guarantee that all ``TestCase`` code starts with a clean database, the Django test runner reorders tests in the following way: -* First, all unittests (including :class:`unittest.TestCase`, - :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and - :class:`~django.test.TransactionTestCase`) are run with no particular ordering - guaranteed nor enforced among them. +* All :class:`~django.test.TestCase` subclasses are run first. + +* Then, all other unittests (including :class:`unittest.TestCase`, + :class:`~django.test.SimpleTestCase` and + :class:`~django.test.TransactionTestCase`) are run with no particular + ordering guaranteed nor enforced among them. * Then any other tests (e.g. doctests) that may alter the database without restoring it to its original state are run. diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 5cc6f6251a..5ec4fbb544 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -137,7 +137,7 @@ class Thing(models.Model): class Actor(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() - title = models.CharField(max_length=50, null=True) + title = models.CharField(max_length=50, null=True, blank=True) def __str__(self): return self.name diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 528c728069..235fe0cb54 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -57,7 +57,7 @@ for a staff account. Note that both fields may be case-sensitive." @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class AdminViewBasicTest(TestCase): +class AdminViewBasicTestCase(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml', 'admin-views-books.xml'] @@ -92,6 +92,7 @@ class AdminViewBasicTest(TestCase): failing_msg ) +class AdminViewBasicTest(AdminViewBasicTestCase): def testTrailingSlashRequired(self): """ If you leave off the trailing slash, app should redirect and add it. @@ -583,6 +584,14 @@ class AdminViewBasicTest(TestCase): response = self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") self.assertEqual(response.status_code, 200) + def test_popup_dismiss_related(self): + """ + Regression test for ticket 20664 - ensure the pk is properly quoted. + """ + actor = Actor.objects.create(name="Palin", age=27) + response = self.client.get("/test_admin/admin/admin_views/actor/?%s" % IS_POPUP_VAR) + self.assertContains(response, "opener.dismissRelatedLookupPopup(window, '%s')" % actor.pk) + def test_hide_change_password(self): """ Tests if the "change password" link in the admin is hidden if the User @@ -753,7 +762,7 @@ class SaveAsTests(TestCase): self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/') -class CustomModelAdminTest(AdminViewBasicTest): +class CustomModelAdminTest(AdminViewBasicTestCase): urls = "admin_views.urls" urlbit = "admin2" @@ -2554,6 +2563,17 @@ action) '/test_admin/admin/admin_views/subscriber/?%s' % IS_POPUP_VAR) self.assertEqual(response.context["action_form"], None) + def test_popup_template_response(self): + """ + Success on popups shall be rendered from template in order to allow + easy customization. + """ + response = self.client.post( + '/test_admin/admin/admin_views/actor/add/?%s=1' % IS_POPUP_VAR, + {'name': 'Troy McClure', 'age': '55', IS_POPUP_VAR: '1'}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.template_name, 'admin/popup_response.html') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestCustomChangeList(TestCase): @@ -4275,7 +4295,7 @@ class AdminKeepChangeListFiltersTests(TestCase): # Get the `add_view`. response = self.client.get(self.get_add_url()) self.assertEqual(response.status_code, 200) - + # Check the form action. form_action = """
    """ % self.get_preserved_filters_querystring() self.assertContains(response, form_action, count=1) diff --git a/tests/backends/tests.py b/tests/backends/tests.py index b6e3e9e36f..c1a26df7fc 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -11,9 +11,12 @@ from django.core.management.color import no_style from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, transaction) from django.db.backends.signals import connection_created +from django.db.backends.sqlite3.base import DatabaseOperations from django.db.backends.postgresql_psycopg2 import version as pg_version from django.db.backends.util import format_number from django.db.models import Sum, Avg, Variance, StdDev +from django.db.models.fields import (AutoField, DateField, DateTimeField, + DecimalField, IntegerField, TimeField) from django.db.utils import ConnectionHandler from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) @@ -402,7 +405,7 @@ class EscapingChecksDebug(EscapingChecks): pass -class SqlliteAggregationTests(TestCase): +class SqliteAggregationTests(TestCase): """ #19360: Raise NotImplementedError when aggregating on date/time fields. """ @@ -418,16 +421,59 @@ class SqlliteAggregationTests(TestCase): models.Item.objects.all().aggregate, aggregate('last_modified')) +class SqliteChecks(TestCase): + + @unittest.skipUnless(connection.vendor == 'sqlite', + "No need to do SQLite checks") + def test_convert_values_to_handle_null_value(self): + database_operations = DatabaseOperations(connection) + self.assertEqual( + None, + database_operations.convert_values(None, AutoField(primary_key=True)) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DateField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DateTimeField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, DecimalField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, IntegerField()) + ) + self.assertEqual( + None, + database_operations.convert_values(None, TimeField()) + ) + + class BackendTestCase(TestCase): def create_squares_with_executemany(self, args): + self.create_squares(args, 'format', True) + + def create_squares(self, args, paramstyle, multiple): cursor = connection.cursor() opts = models.Square._meta tbl = connection.introspection.table_name_converter(opts.db_table) f1 = connection.ops.quote_name(opts.get_field('root').column) f2 = connection.ops.quote_name(opts.get_field('square').column) - query = 'INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (tbl, f1, f2) - cursor.executemany(query, args) + if paramstyle=='format': + query = 'INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % (tbl, f1, f2) + elif paramstyle=='pyformat': + query = 'INSERT INTO %s (%s, %s) VALUES (%%(root)s, %%(square)s)' % (tbl, f1, f2) + else: + raise ValueError("unsupported paramstyle in test") + if multiple: + cursor.executemany(query, args) + else: + cursor.execute(query, args) def test_cursor_executemany(self): #4896: Test cursor.executemany @@ -456,6 +502,35 @@ class BackendTestCase(TestCase): self.create_squares_with_executemany(args) self.assertEqual(models.Square.objects.count(), 9) + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_execute_with_pyformat(self): + #10070: Support pyformat style passing of paramters + args = {'root': 3, 'square': 9} + self.create_squares(args, 'pyformat', multiple=False) + self.assertEqual(models.Square.objects.count(), 1) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_executemany_with_pyformat(self): + #10070: Support pyformat style passing of paramters + args = [{'root': i, 'square': i**2} for i in range(-5, 6)] + self.create_squares(args, 'pyformat', multiple=True) + self.assertEqual(models.Square.objects.count(), 11) + for i in range(-5, 6): + square = models.Square.objects.get(root=i) + self.assertEqual(square.square, i**2) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def test_cursor_executemany_with_pyformat_iterator(self): + args = iter({'root': i, 'square': i**2} for i in range(-3, 2)) + self.create_squares(args, 'pyformat', multiple=True) + self.assertEqual(models.Square.objects.count(), 5) + + args = iter({'root': i, 'square': i**2} for i in range(3, 7)) + with override_settings(DEBUG=True): + # same test for DebugCursorWrapper + self.create_squares(args, 'pyformat', multiple=True) + self.assertEqual(models.Square.objects.count(), 9) + def test_unicode_fetches(self): #6254: fetchone, fetchmany, fetchall return strings as unicode objects qn = connection.ops.quote_name diff --git a/tests/check/__init__.py b/tests/check/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/compat_checks/models.py b/tests/check/models.py similarity index 100% rename from tests/compat_checks/models.py rename to tests/check/models.py diff --git a/tests/compat_checks/tests.py b/tests/check/tests.py similarity index 86% rename from tests/compat_checks/tests.py rename to tests/check/tests.py index 879988c905..98495e38ae 100644 --- a/tests/compat_checks/tests.py +++ b/tests/check/tests.py @@ -1,6 +1,6 @@ -from django.core.compat_checks import base -from django.core.compat_checks import django_1_6_0 -from django.core.management.commands import checksetup +from django.core.checks.compatibility import base +from django.core.checks.compatibility import django_1_6_0 +from django.core.management.commands import check from django.core.management import call_command from django.test import TestCase @@ -86,22 +86,22 @@ class CompatChecksTestCase(TestCase): def test_management_command(self): # Again, we unfortunately have to patch out ``warnings``. Different - old_warnings = checksetup.warnings - checksetup.warnings = FakeWarnings() + old_warnings = check.warnings + check.warnings = FakeWarnings() - self.assertEqual(len(checksetup.warnings._warnings), 0) + self.assertEqual(len(check.warnings._warnings), 0) # Should not produce any warnings. with self.settings(TEST_RUNNER='myapp.test.CustomRunnner'): - call_command('checksetup') + call_command('check') - self.assertEqual(len(checksetup.warnings._warnings), 0) + self.assertEqual(len(check.warnings._warnings), 0) with self.settings(TEST_RUNNER='django.test.runner.DiscoverRunner'): - call_command('checksetup') + call_command('check') - self.assertEqual(len(checksetup.warnings._warnings), 1) - self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in checksetup.warnings._warnings[0]) + self.assertEqual(len(check.warnings._warnings), 1) + self.assertTrue("You have not explicitly set 'TEST_RUNNER'" in check.warnings._warnings[0]) # Restore the ``warnings``. base.warnings = old_warnings diff --git a/tests/field_deconstruction/tests.py b/tests/field_deconstruction/tests.py index 683854940b..4ccf4d048c 100644 --- a/tests/field_deconstruction/tests.py +++ b/tests/field_deconstruction/tests.py @@ -108,7 +108,7 @@ class FieldDeconstructionTests(TestCase): name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.EmailField") self.assertEqual(args, []) - self.assertEqual(kwargs, {}) + self.assertEqual(kwargs, {"max_length": 75}) field = models.EmailField(max_length=255) name, path, args, kwargs = field.deconstruct() self.assertEqual(path, "django.db.models.EmailField") diff --git a/tests/files/tests.py b/tests/files/tests.py index cd2d15acdb..f1e3d5b14b 100644 --- a/tests/files/tests.py +++ b/tests/files/tests.py @@ -1,11 +1,13 @@ from __future__ import absolute_import +import os import gzip import shutil import tempfile from django.core.cache import cache from django.core.files import File +from django.core.files.move import file_move_safe from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase @@ -146,3 +148,15 @@ class FileTests(unittest.TestCase): file = SimpleUploadedFile("mode_test.txt", b"content") self.assertFalse(hasattr(file, 'mode')) g = gzip.GzipFile(fileobj=file) + + +class FileMoveSafeTests(unittest.TestCase): + def test_file_move_overwrite(self): + handle_a, self.file_a = tempfile.mkstemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) + handle_b, self.file_b = tempfile.mkstemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) + + # file_move_safe should raise an IOError exception if destination file exists and allow_overwrite is False + self.assertRaises(IOError, lambda: file_move_safe(self.file_a, self.file_b, allow_overwrite=False)) + + # should allow it and continue on if allow_overwrite is True + self.assertIsNone(file_move_safe(self.file_a, self.file_b, allow_overwrite=True)) diff --git a/tests/foreign_object/models.py b/tests/foreign_object/models.py index 2d02b5624c..eee8091a15 100644 --- a/tests/foreign_object/models.py +++ b/tests/foreign_object/models.py @@ -150,3 +150,11 @@ class ArticleTranslation(models.Model): class Meta: unique_together = ('article', 'lang') ordering = ('active_translation__title',) + +class ArticleTag(models.Model): + article = models.ForeignKey(Article, related_name="tags", related_query_name="tag") + name = models.CharField(max_length=255) + +class ArticleIdea(models.Model): + articles = models.ManyToManyField(Article, related_name="ideas", related_query_name="idea_things") + name = models.CharField(max_length=255) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 69636ee49b..670fc94dc5 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -1,9 +1,10 @@ import datetime from operator import attrgetter -from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation +from .models import Country, Person, Group, Membership, Friendship, Article, ArticleTranslation, ArticleTag, ArticleIdea from django.test import TestCase from django.utils.translation import activate +from django.core.exceptions import FieldError from django import forms class MultiColumnFKTests(TestCase): @@ -321,6 +322,24 @@ class MultiColumnFKTests(TestCase): with self.assertRaisesMessage(Article.DoesNotExist, 'ArticleTranslation has no article'): referrer.article + def test_foreign_key_related_query_name(self): + a1 = Article.objects.create(pub_date=datetime.date.today()) + ArticleTag.objects.create(article=a1, name="foo") + self.assertEqual(Article.objects.filter(tag__name="foo").count(), 1) + self.assertEqual(Article.objects.filter(tag__name="bar").count(), 0) + with self.assertRaises(FieldError): + Article.objects.filter(tags__name="foo") + + def test_many_to_many_related_query_name(self): + a1 = Article.objects.create(pub_date=datetime.date.today()) + i1 = ArticleIdea.objects.create(name="idea1") + a1.ideas.add(i1) + self.assertEqual(Article.objects.filter(idea_things__name="idea1").count(), 1) + self.assertEqual(Article.objects.filter(idea_things__name="idea2").count(), 0) + with self.assertRaises(FieldError): + Article.objects.filter(ideas__name="idea1") + + class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs # to manually deal with the foreignobject relation. diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 47c637befa..f7d83503f4 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -296,7 +296,7 @@ class FieldsTests(SimpleTestCase): def test_decimalfield_1(self): f = DecimalField(max_digits=4, decimal_places=2) - self.assertWidgetRendersTo(f, '') + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual(f.clean('1'), Decimal("1")) @@ -342,7 +342,7 @@ class FieldsTests(SimpleTestCase): def test_decimalfield_3(self): f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5')) - self.assertWidgetRendersTo(f, '') + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'Ensure this value is less than or equal to 1.5.'", f.clean, '1.6') self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 0.5.'", f.clean, '0.4') self.assertEqual(f.clean('1.5'), Decimal("1.5")) diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index b26017bc78..41577e6049 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -1072,7 +1072,8 @@ ArticleFormSet = formset_factory(ArticleForm) class TestIsBoundBehavior(TestCase): def test_no_data_raises_validation_error(self): - self.assertRaises(ValidationError, ArticleFormSet, {}) + with self.assertRaises(ValidationError): + ArticleFormSet({}).is_valid() def test_with_management_data_attrs_work_fine(self): data = { diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index 435e48ba99..84d18ebcb2 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -7,8 +7,9 @@ from django.core.urlresolvers import reverse from django import forms from django.test import TestCase from django.utils.unittest import expectedFailure +from django.test.client import RequestFactory from django.views.generic.base import View -from django.views.generic.edit import FormMixin, CreateView, UpdateView +from django.views.generic.edit import FormMixin, CreateView from . import views from .models import Artist, Author @@ -22,6 +23,24 @@ class FormMixinTests(TestCase): initial_2 = FormMixin().get_initial() self.assertNotEqual(initial_1, initial_2) + def test_get_prefix(self): + """ Test prefix can be set (see #18872) """ + test_string = 'test' + + rf = RequestFactory() + get_request = rf.get('/') + + class TestFormMixin(FormMixin): + request = get_request + + default_kwargs = TestFormMixin().get_form_kwargs() + self.assertEqual(None, default_kwargs.get('prefix')) + + set_mixin = TestFormMixin() + set_mixin.prefix = test_string + set_kwargs = set_mixin.get_form_kwargs() + self.assertEqual(test_string, set_kwargs.get('prefix')) + class BasicFormTests(TestCase): urls = 'generic_views.urls' diff --git a/tests/i18n/commands/__init__.py b/tests/i18n/commands/__init__.py index ffd439140e..76249ca34d 100644 --- a/tests/i18n/commands/__init__.py +++ b/tests/i18n/commands/__init__.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _, ungettext # Translators: This comment should be extracted dummy1 = _("This is a translatable string.") @@ -6,3 +6,7 @@ dummy1 = _("This is a translatable string.") # This comment should not be extracted dummy2 = _("This is another translatable string.") +# This file has a literal with plural forms. When processed first, makemessages +# shouldn't create a .po file with duplicate `Plural-Forms` headers +number = 3 +dummuy3 = ungettext("%(number)s Foo", "%(number)s Foos", number) % {'number': number} diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 8696ae453b..dd1dce2ed0 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -329,6 +329,15 @@ class SymlinkExtractorTests(ExtractorTests): class CopyPluralFormsExtractorTests(ExtractorTests): + PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po' + + def tearDown(self): + os.chdir(self.test_dir) + try: + self._rmrf('locale/es') + except OSError: + pass + os.chdir(self._cwd) def test_copy_plural_forms(self): os.chdir(self.test_dir) @@ -338,6 +347,16 @@ class CopyPluralFormsExtractorTests(ExtractorTests): po_contents = force_text(fp.read()) self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents) + def test_override_plural_forms(self): + """Ticket #20311.""" + os.chdir(self.test_dir) + management.call_command('makemessages', locale='es', extensions=['djtpl'], verbosity=0) + self.assertTrue(os.path.exists(self.PO_FILE_ES)) + with open(self.PO_FILE_ES, 'r') as fp: + po_contents = force_text(fp.read()) + found = re.findall(r'^(?P"Plural-Forms.+?\\n")\s*$', po_contents, re.MULTILINE | re.DOTALL) + self.assertEqual(1, len(found)) + class NoWrapExtractorTests(ExtractorTests): diff --git a/tests/i18n/commands/templates/plural.djtpl b/tests/i18n/commands/templates/plural.djtpl new file mode 100644 index 0000000000..b2f2a18750 --- /dev/null +++ b/tests/i18n/commands/templates/plural.djtpl @@ -0,0 +1,6 @@ +{% load i18n %} +{% comment %} +This file has a literal with plural forms. When processed first, makemessages +shouldn't create a .po file with duplicate `Plural-Forms` headers +{% endcomment %} +{% blocktrans count number=3 %}{{ number }} Bar{% plural %}{{ number }} Bars{% endblocktrans %} diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index de7105f92d..ee9c5afe1d 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -610,6 +610,28 @@ class LookupTests(TestCase): self.assertQuerysetEqual(Article.objects.filter(headline__regex=r'b(.).*b\1'), ['', '', '']) + def test_regex_null(self): + """ + Ensure that a regex lookup does not fail on null/None values + """ + Season.objects.create(year=2012, gt=None) + self.assertQuerysetEqual(Season.objects.filter(gt__regex=r'^$'), []) + + def test_regex_non_string(self): + """ + Ensure that a regex lookup does not fail on non-string fields + """ + Season.objects.create(year=2013, gt=444) + self.assertQuerysetEqual(Season.objects.filter(gt__regex=r'^444$'), + ['']) + + def test_regex_non_ascii(self): + """ + Ensure that a regex lookup does not trip on non-ascii characters. + """ + Player.objects.create(name='\u2660') + Player.objects.get(name__regex='\u2660') + def test_nonfield_lookups(self): """ Ensure that a lookup query containing non-fields raises the proper diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 03cd3b0159..43509c471f 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -8,7 +8,7 @@ from decimal import Decimal from django import forms from django.db import models from django.forms.models import (_get_foreign_key, inlineformset_factory, - modelformset_factory) + modelformset_factory, BaseModelFormSet) from django.test import TestCase, skipUnlessDBFeature from django.utils import six @@ -386,6 +386,23 @@ class ModelFormsetTest(TestCase): formset = PostFormSet() self.assertFalse("subtitle" in formset.forms[0].fields) + def test_custom_queryset_init(self): + """ + Test that a queryset can be overriden in the __init__ method. + https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#changing-the-queryset + """ + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + + class BaseAuthorFormSet(BaseModelFormSet): + def __init__(self, *args, **kwargs): + super(BaseAuthorFormSet, self).__init__(*args, **kwargs) + self.queryset = Author.objects.filter(name__startswith='Charles') + + AuthorFormSet = modelformset_factory(Author, formset=BaseAuthorFormSet) + formset = AuthorFormSet() + self.assertEqual(len(formset.get_queryset()), 1) + def test_model_inheritance(self): BetterAuthorFormSet = modelformset_factory(BetterAuthor, fields="__all__") formset = BetterAuthorFormSet() @@ -559,7 +576,7 @@ class ModelFormsetTest(TestCase): formset = AuthorBooksFormSet2(instance=author) self.assertEqual(len(formset.forms), 1) self.assertHTMLEqual(formset.forms[0].as_p(), - '

    \n' + '

    \n' '

    ') data = { diff --git a/tests/raw_query/tests.py b/tests/raw_query/tests.py index e404c8b065..7242b8309b 100644 --- a/tests/raw_query/tests.py +++ b/tests/raw_query/tests.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from datetime import date from django.db.models.query_utils import InvalidQuery -from django.test import TestCase +from django.test import TestCase, skipUnlessDBFeature from .models import Author, Book, Coffee, Reviewer, FriendlyAuthor @@ -123,10 +123,27 @@ class RawQueryTests(TestCase): query = "SELECT * FROM raw_query_author WHERE first_name = %s" author = Author.objects.all()[2] params = [author.first_name] - results = list(Author.objects.raw(query, params=params)) + qset = Author.objects.raw(query, params=params) + results = list(qset) self.assertProcessed(Author, results, [author]) self.assertNoAnnotations(results) self.assertEqual(len(results), 1) + self.assertIsInstance(repr(qset), str) + + @skipUnlessDBFeature('supports_paramstyle_pyformat') + def testPyformatParams(self): + """ + Test passing optional query parameters + """ + query = "SELECT * FROM raw_query_author WHERE first_name = %(first)s" + author = Author.objects.all()[2] + params = {'first': author.first_name} + qset = Author.objects.raw(query, params=params) + results = list(qset) + self.assertProcessed(Author, results, [author]) + self.assertNoAnnotations(results) + self.assertEqual(len(results), 1) + self.assertIsInstance(repr(qset), str) def testManyToMany(self): """ diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 3377793d68..be74afe820 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -95,6 +95,11 @@ class LiveServerAddress(LiveServerBase): else: del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] + @classmethod + def tearDownClass(cls): + # skip it, as setUpClass doesn't call its parent either + pass + @classmethod def raises_exception(cls, address, exception): os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index f8d66bb2b7..625c4f31df 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -9,17 +9,19 @@ from django.test.utils import override_settings from django.utils import unittest, six -@override_settings(TEST='override') +@override_settings(TEST='override', TEST_OUTER='outer') class FullyDecoratedTranTestCase(TransactionTestCase): available_apps = [] def test_override(self): self.assertEqual(settings.TEST, 'override') + self.assertEqual(settings.TEST_OUTER, 'outer') @override_settings(TEST='override2') def test_method_override(self): self.assertEqual(settings.TEST, 'override2') + self.assertEqual(settings.TEST_OUTER, 'outer') def test_decorated_testcase_name(self): self.assertEqual(FullyDecoratedTranTestCase.__name__, 'FullyDecoratedTranTestCase') @@ -168,6 +170,29 @@ class SettingsTests(TestCase): self.assertRaises(AttributeError, getattr, settings, 'USE_I18N') self.assertEqual(settings.USE_I18N, previous_i18n) + def test_override_settings_nested(self): + """ + Test that override_settings uses the actual _wrapped attribute at + runtime, not when it was instantiated. + """ + + self.assertRaises(AttributeError, getattr, settings, 'TEST') + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + + inner = override_settings(TEST2='override') + with override_settings(TEST='override'): + self.assertEqual('override', settings.TEST) + with inner: + self.assertEqual('override', settings.TEST) + self.assertEqual('override', settings.TEST2) + # inner's __exit__ should have restored the settings of the outer + # context manager, not those when the class was instantiated + self.assertEqual('override', settings.TEST) + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + + self.assertRaises(AttributeError, getattr, settings, 'TEST') + self.assertRaises(AttributeError, getattr, settings, 'TEST2') + def test_allowed_include_roots_string(self): """ ALLOWED_INCLUDE_ROOTS is not allowed to be incorrectly set to a string diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 206c648398..76712a09a6 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -854,6 +854,10 @@ class TemplateTests(TransRealMixin, TestCase): 'filter02': ('{% filter upper %}django{% endfilter %}', {}, 'DJANGO'), 'filter03': ('{% filter upper|lower %}django{% endfilter %}', {}, 'django'), 'filter04': ('{% filter cut:remove %}djangospam{% endfilter %}', {'remove': 'spam'}, 'django'), + 'filter05': ('{% filter safe %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter05bis': ('{% filter upper|safe %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter06': ('{% filter escape %}fail{% endfilter %}', {}, template.TemplateSyntaxError), + 'filter06bis': ('{% filter upper|escape %}fail{% endfilter %}', {}, template.TemplateSyntaxError), ### FIRSTOF TAG ########################################################### 'firstof01': ('{% firstof a b c %}', {'a':0,'b':0,'c':0}, ''), diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 24b7615d6f..756fa40abd 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,9 +1,8 @@ from __future__ import absolute_import import sys -import warnings -from django.db import connection, transaction, IntegrityError +from django.db import connection, transaction, DatabaseError, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six @@ -188,6 +187,29 @@ class AtomicTests(TransactionTestCase): raise Exception("Oops, that's his first name") self.assertQuerysetEqual(Reporter.objects.all(), []) + def test_force_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + # atomic block shouldn't rollback, but force it. + self.assertFalse(transaction.get_rollback()) + transaction.set_rollback(True) + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_prevent_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + sid = transaction.savepoint() + # trigger a database error inside an inner atomic without savepoint + with self.assertRaises(DatabaseError): + with transaction.atomic(savepoint=False): + connection.cursor().execute( + "SELECT no_such_col FROM transactions_reporter") + transaction.savepoint_rollback(sid) + # atomic block should rollback, but prevent it, as we just did it. + self.assertTrue(transaction.get_rollback()) + transaction.set_rollback(False) + self.assertQuerysetEqual(Reporter.objects.all(), ['']) + class AtomicInsideTransactionTests(AtomicTests): """All basic tests for atomic should also pass within an existing transaction."""