diff --git a/AUTHORS b/AUTHORS index 5fa957885c53d..0a3699d5163cf 100644 --- a/AUTHORS +++ b/AUTHORS @@ -506,6 +506,7 @@ answer newbie questions, and generally made Django that much better: Johan C. Stöver Nowell Strite Thomas Stromberg + Travis Swicegood Pascal Varet SuperJared Radek Švarz diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f4d17ca9f3a0b..6272f4ed5d237 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -7,7 +7,6 @@ """ import os -import re import time # Needed for Windows import warnings @@ -26,7 +25,7 @@ class LazySettings(LazyObject): The user can manually configure settings prior to using them. Otherwise, Django uses the settings module pointed to by DJANGO_SETTINGS_MODULE. """ - def _setup(self): + def _setup(self, name): """ Load the settings module pointed to by the environment variable. This is used the first time we need any settings at all, if the user has not @@ -37,12 +36,21 @@ def _setup(self): if not settings_module: # If it's set but is an empty string. raise KeyError except KeyError: - # NOTE: This is arguably an EnvironmentError, but that causes - # problems with Python's interactive help. - raise ImportError("Settings cannot be imported, because environment variable %s is undefined." % ENVIRONMENT_VARIABLE) + raise ImproperlyConfigured( + "Requested setting %s, but settings are not configured. " + "You must either define the environment variable %s " + "or call settings.configure() before accessing settings." + % (name, ENVIRONMENT_VARIABLE)) self._wrapped = Settings(settings_module) + + def __getattr__(self, name): + if self._wrapped is empty: + self._setup(name) + return getattr(self._wrapped, name) + + def configure(self, default_settings=global_settings, **options): """ Called to manually configure the settings. The 'default_settings' diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 081d00121b1ea..f4205f2ce7b45 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -14,9 +14,10 @@ from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db import models, transaction, router +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 LOOKUP_SEP, QUERY_TERMS +from django.db.models.sql.constants import QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse @@ -1456,8 +1457,10 @@ def has_delete_permission(self, request, obj=None): return request.user.has_perm( self.opts.app_label + '.' + self.opts.get_delete_permission()) + class StackedInline(InlineModelAdmin): template = 'admin/edit_inline/stacked.html' + class TabularInline(InlineModelAdmin): template = 'admin/edit_inline/tabular.html' diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index c11af82f76afe..4dc9459ff3661 100644 --- a/django/contrib/admin/static/admin/js/inlines.js +++ b/django/contrib/admin/static/admin/js/inlines.js @@ -9,128 +9,264 @@ * All rights reserved. * * Spiced up with Code from Zain Memon's GSoC project 2009 - * and modified for Django by Jannis Leidel + * and modified for Django by Jannis Leidel, Travis Swicegood and Julien Phalip. * * Licensed under the New BSD License * See: http://www.opensource.org/licenses/bsd-license.php */ (function($) { - $.fn.formset = function(opts) { - var options = $.extend({}, $.fn.formset.defaults, opts); - var updateElementIndex = function(el, prefix, ndx) { - var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); - var replacement = prefix + "-" + ndx; - if ($(el).attr("for")) { - $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); - } - if (el.id) { - el.id = el.id.replace(id_regex, replacement); - } - if (el.name) { - el.name = el.name.replace(id_regex, replacement); - } - }; - var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); - var nextIndex = parseInt(totalForms.val(), 10); - var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); - // only show the add button if we are allowed to add more items, + $.fn.formset = function(opts) { + var options = $.extend({}, $.fn.formset.defaults, opts); + var $this = $(this); + var $parent = $this.parent(); + var updateElementIndex = function(el, prefix, ndx) { + var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); + var replacement = prefix + "-" + ndx; + if ($(el).attr("for")) { + $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); + } + if (el.id) { + el.id = el.id.replace(id_regex, replacement); + } + if (el.name) { + el.name = el.name.replace(id_regex, replacement); + } + }; + var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); + var nextIndex = parseInt(totalForms.val(), 10); + var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); + // only show the add button if we are allowed to add more items, // note that max_num = None translates to a blank string. - var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; - $(this).each(function(i) { - $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); - }); - if ($(this).length && showAddButton) { - var addButton; - if ($(this).attr("tagName") == "TR") { - // If forms are laid out as table rows, insert the - // "add" button in a new table row: - var numCols = this.eq(-1).children().length; - $(this).parent().append('' + options.addText + ""); - addButton = $(this).parent().find("tr:last a"); - } else { - // Otherwise, insert it immediately after the last form: - $(this).filter(":last").after('"); - addButton = $(this).filter(":last").next().find("a"); - } - addButton.click(function(e) { - e.preventDefault(); - var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); - var template = $("#" + options.prefix + "-empty"); - var row = template.clone(true); - row.removeClass(options.emptyCssClass) - .addClass(options.formCssClass) - .attr("id", options.prefix + "-" + nextIndex); - if (row.is("tr")) { - // If the forms are laid out in table rows, insert - // the remove button into the last table cell: - row.children(":last").append('"); - } else if (row.is("ul") || row.is("ol")) { - // If they're laid out as an ordered/unordered list, - // insert an
  • after the last list item: - row.append('
  • ' + options.deleteText + "
  • "); - } else { - // Otherwise, just insert the remove button as the - // last child element of the form's container: - row.children(":first").append('' + options.deleteText + ""); - } - row.find("*").each(function() { - updateElementIndex(this, options.prefix, totalForms.val()); - }); - // Insert the new form when it has been fully edited - row.insertBefore($(template)); - // Update number of total forms - $(totalForms).val(parseInt(totalForms.val(), 10) + 1); - nextIndex += 1; - // Hide add button in case we've hit the max, except we want to add infinitely - if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { - addButton.parent().hide(); - } - // The delete button of each row triggers a bunch of other things - row.find("a." + options.deleteCssClass).click(function(e) { - e.preventDefault(); - // Remove the parent form containing this button: - var row = $(this).parents("." + options.formCssClass); - row.remove(); - nextIndex -= 1; - // If a post-delete callback was provided, call it with the deleted form: - if (options.removed) { - options.removed(row); - } - // Update the TOTAL_FORMS form count. - var forms = $("." + options.formCssClass); - $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); - // Show add button again once we drop below max - if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { - addButton.parent().show(); - } - // Also, update names and ids for all remaining form controls - // so they remain in sequence: - for (var i=0, formCount=forms.length; i 0; + $this.each(function(i) { + $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); + }); + if ($this.length && showAddButton) { + var addButton; + if ($this.attr("tagName") == "TR") { + // If forms are laid out as table rows, insert the + // "add" button in a new table row: + var numCols = this.eq(-1).children().length; + $parent.append('' + options.addText + ""); + addButton = $parent.find("tr:last a"); + } else { + // Otherwise, insert it immediately after the last form: + $this.filter(":last").after('"); + addButton = $this.filter(":last").next().find("a"); + } + addButton.click(function(e) { + e.preventDefault(); + var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); + var template = $("#" + options.prefix + "-empty"); + var row = template.clone(true); + row.removeClass(options.emptyCssClass) + .addClass(options.formCssClass) + .attr("id", options.prefix + "-" + nextIndex); + if (row.is("tr")) { + // If the forms are laid out in table rows, insert + // the remove button into the last table cell: + row.children(":last").append('"); + } else if (row.is("ul") || row.is("ol")) { + // If they're laid out as an ordered/unordered list, + // insert an
  • after the last list item: + row.append('
  • ' + options.deleteText + "
  • "); + } else { + // Otherwise, just insert the remove button as the + // last child element of the form's container: + row.children(":first").append('' + options.deleteText + ""); + } + row.find("*").each(function() { + updateElementIndex(this, options.prefix, totalForms.val()); + }); + // Insert the new form when it has been fully edited + row.insertBefore($(template)); + // Update number of total forms + $(totalForms).val(parseInt(totalForms.val(), 10) + 1); + nextIndex += 1; + // Hide add button in case we've hit the max, except we want to add infinitely + if ((maxForms.val() !== '') && (maxForms.val()-totalForms.val()) <= 0) { + addButton.parent().hide(); + } + // The delete button of each row triggers a bunch of other things + row.find("a." + options.deleteCssClass).click(function(e) { + e.preventDefault(); + // Remove the parent form containing this button: + var row = $(this).parents("." + options.formCssClass); + row.remove(); + nextIndex -= 1; + // If a post-delete callback was provided, call it with the deleted form: + if (options.removed) { + options.removed(row); + } + // Update the TOTAL_FORMS form count. + var forms = $("." + options.formCssClass); + $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length); + // Show add button again once we drop below max + if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { + addButton.parent().show(); + } + // Also, update names and ids for all remaining form controls + // so they remain in sequence: + for (var i=0, formCount=forms.length; i0;b(this).each(function(){b(this).not("."+ -a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&g){var j;if(b(this).attr("tagName")=="TR"){g=this.eq(-1).children().length;b(this).parent().append(''+a.addText+"");j=b(this).parent().find("tr:last a")}else{b(this).filter(":last").after('");j=b(this).filter(":last").next().find("a")}j.click(function(c){c.preventDefault(); -var f=b("#id_"+a.prefix+"-TOTAL_FORMS");c=b("#"+a.prefix+"-empty");var e=c.clone(true);e.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);if(e.is("tr"))e.children(":last").append('");else e.is("ul")||e.is("ol")?e.append('
  • '+a.deleteText+"
  • "):e.children(":first").append(''+ -a.deleteText+"");e.find("*").each(function(){k(this,a.prefix,f.val())});e.insertBefore(b(c));b(f).val(parseInt(f.val(),10)+1);l+=1;h.val()!==""&&h.val()-f.val()<=0&&j.parent().hide();e.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();l-=1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);if(h.val()===""||h.val()-d.length>0)j.parent().show();for(var i=0,m=d.length;i'+a.addText+""),h=d.find("tr:last a")):(c.filter(":last").after('"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+ +"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('"):c.is("ul")||c.is("ol")?c.append('
  • '+a.deleteText+"
  • "):c.children(":first").append(''+a.deleteText+"");c.find("*").each(function(){i(this, +a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c{{ inline_admin_formset.opts.verbose_name|title }}:  (function($) { - $(document).ready(function() { - var rows = "#{{ inline_admin_formset.formset.prefix }}-group .inline-related"; - var updateInlineLabel = function(row) { - $(rows).find(".inline_label").each(function(i) { - var count = i + 1; - $(this).html($(this).html().replace(/(#\d+)/g, "#" + count)); - }); - }; - var reinitDateTimeShortCuts = function() { - // Reinitialize the calendar and clock widgets by force, yuck. - if (typeof DateTimeShortcuts != "undefined") { - $(".datetimeshortcuts").remove(); - DateTimeShortcuts.init(); - } - }; - var updateSelectFilter = function() { - // If any SelectFilter widgets were added, instantiate a new instance. - if (typeof SelectFilter != "undefined"){ - $(".selectfilter").each(function(index, value){ - var namearr = value.name.split('-'); - SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% static "admin/" %}"); - }); - $(".selectfilterstacked").each(function(index, value){ - var namearr = value.name.split('-'); - SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% static "admin/" %}"); - }); - } - }; - var initPrepopulatedFields = function(row) { - row.find('.prepopulated_field').each(function() { - var field = $(this); - var input = field.find('input, select, textarea'); - var dependency_list = input.data('dependency_list') || []; - var dependencies = []; - $.each(dependency_list, function(i, field_name) { - dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id')); - }); - if (dependencies.length) { - input.prepopulate(dependencies, input.attr('maxlength')); - } - }); - }; - $(rows).formset({ - prefix: "{{ inline_admin_formset.formset.prefix }}", - addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}", - formCssClass: "dynamic-{{ inline_admin_formset.formset.prefix }}", - deleteCssClass: "inline-deletelink", - deleteText: "{% trans "Remove" %}", - emptyCssClass: "empty-form", - removed: updateInlineLabel, - added: (function(row) { - initPrepopulatedFields(row); - reinitDateTimeShortCuts(); - updateSelectFilter(); - updateInlineLabel(row); - }) - }); - }); + $("#{{ inline_admin_formset.formset.prefix }}-group .inline-related").stackedFormset({ + prefix: '{{ inline_admin_formset.formset.prefix }}', + adminStaticPrefix: '{% static "admin/" %}', + deleteText: "{% trans "Remove" %}", + addText: "{% blocktrans with verbose_name=inline_admin_formset.opts.verbose_name|title %}Add another {{ verbose_name }}{% endblocktrans %}" + }); })(django.jQuery); diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html index 4f4915381999d..f2757ede48820 100644 --- a/django/contrib/admin/templates/admin/edit_inline/tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html @@ -67,64 +67,13 @@

    {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 1873d4498983d..ce435dea81858 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -182,7 +182,7 @@ def items_for_result(cl, result, form): row_class = '' try: f, attr, value = lookup_field(field_name, result, cl.model_admin) - except (AttributeError, ObjectDoesNotExist): + except ObjectDoesNotExist: result_repr = EMPTY_CHANGELIST_VALUE else: if f is None: diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 889f692ac3669..f95fe53de188a 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -4,7 +4,7 @@ import decimal from django.db import models -from django.db.models.sql.constants import LOOKUP_SEP +from django.db.models.constants import LOOKUP_SEP from django.db.models.deletion import Collector from django.db.models.related import RelatedObject from django.forms.forms import pretty_name diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index beeb284998cdb..11518193e7cc8 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -8,6 +8,7 @@ from django.core.exceptions import PermissionDenied from django.utils.decorators import available_attrs from django.utils.encoding import force_str +from django.shortcuts import resolve_url def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): @@ -23,17 +24,19 @@ def _wrapped_view(request, *args, **kwargs): if test_func(request.user): return view_func(request, *args, **kwargs) path = request.build_absolute_uri() - # urlparse chokes on lazy objects in Python 3 - login_url_as_str = force_str(login_url or settings.LOGIN_URL) + # urlparse chokes on lazy objects in Python 3, force to str + resolved_login_url = force_str( + resolve_url(login_url or settings.LOGIN_URL)) # If the login url is the same scheme and net location then just # use the path as the "next" url. - login_scheme, login_netloc = urlparse(login_url_as_str)[:2] + login_scheme, login_netloc = urlparse(resolved_login_url)[:2] current_scheme, current_netloc = urlparse(path)[:2] if ((not login_scheme or login_scheme == current_scheme) and (not login_netloc or login_netloc == current_netloc)): path = request.get_full_path() from django.contrib.auth.views import redirect_to_login - return redirect_to_login(path, login_url, redirect_field_name) + return redirect_to_login( + path, resolved_login_url, redirect_field_name) return _wrapped_view return decorator diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index bd3f0115f5aa1..cefc310e40cf0 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -25,7 +25,7 @@ def normal_view(request): pass login_required(normal_view) - def testLoginRequired(self, view_url='/login_required/', login_url=settings.LOGIN_URL): + def testLoginRequired(self, view_url='/login_required/', login_url='/login/'): """ Check that login_required works on a simple view wrapped in a login_required decorator. diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f93541b4bf926..024be5e46d7f8 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -7,9 +7,9 @@ from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect, QueryDict from django.template.response import TemplateResponse -from django.utils.encoding import force_str from django.utils.http import base36_to_int from django.utils.translation import ugettext as _ +from django.shortcuts import resolve_url 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 @@ -38,16 +38,16 @@ def login(request, template_name='registration/login.html', if request.method == "POST": form = authentication_form(data=request.POST) if form.is_valid(): - netloc = urlparse(redirect_to)[1] - # Use default setting if redirect_to is empty if not redirect_to: redirect_to = settings.LOGIN_REDIRECT_URL + redirect_to = resolve_url(redirect_to) + netloc = urlparse(redirect_to)[1] # Heavier security check -- don't allow redirection to a different # host. - elif netloc and netloc != request.get_host(): - redirect_to = settings.LOGIN_REDIRECT_URL + if netloc and netloc != request.get_host(): + redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) # Okay, security checks complete. Log the user in. auth_login(request, form.get_user()) @@ -110,6 +110,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N """ if not login_url: login_url = settings.LOGIN_URL + login_url = resolve_url(login_url) return logout(request, login_url, current_app=current_app, extra_context=extra_context) def redirect_to_login(next, login_url=None, @@ -117,10 +118,9 @@ def redirect_to_login(next, login_url=None, """ Redirects the user to the login page, passing the given 'next' page """ - # urlparse chokes on lazy objects in Python 3 - login_url_as_str = force_str(login_url or settings.LOGIN_URL) + resolved_url = resolve_url(login_url or settings.LOGIN_URL) - login_url_parts = list(urlparse(login_url_as_str)) + login_url_parts = list(urlparse(resolved_url)) if redirect_field_name: querystring = QueryDict(login_url_parts[4], mutable=True) querystring[redirect_field_name] = next @@ -229,7 +229,7 @@ def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): context = { - 'login_url': settings.LOGIN_URL + 'login_url': resolve_url(settings.LOGIN_URL) } if extra_context is not None: context.update(extra_context) diff --git a/django/contrib/gis/db/models/sql/where.py b/django/contrib/gis/db/models/sql/where.py index 0e152221ac75d..ec078aebedfb7 100644 --- a/django/contrib/gis/db/models/sql/where.py +++ b/django/contrib/gis/db/models/sql/where.py @@ -1,5 +1,5 @@ +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -from django.db.models.sql.constants import LOOKUP_SEP from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.where import Constraint, WhereNode from django.contrib.gis.db.models.fields import GeometryField diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 18b7475ca7fc3..389c919c07a5b 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -47,6 +47,9 @@ def markdown(value, arg=''): they will be silently ignored. """ + import warnings + warnings.warn('The markdown filter has been deprecated', + category=DeprecationWarning) try: import markdown except ImportError: @@ -72,6 +75,9 @@ def markdown(value, arg=''): @register.filter(is_safe=True) def restructuredtext(value): + import warnings + warnings.warn('The restructuredtext filter has been deprecated', + category=DeprecationWarning) try: from docutils.core import publish_parts except ImportError: diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py index 7b050ace823e3..19a3b7e9d002d 100644 --- a/django/contrib/markup/tests.py +++ b/django/contrib/markup/tests.py @@ -1,7 +1,9 @@ # Quick tests for the markup templatetags (django.contrib.markup) import re +import warnings from django.template import Template, Context +from django import test from django.utils import unittest from django.utils.html import escape @@ -21,7 +23,7 @@ except ImportError: docutils = None -class Templates(unittest.TestCase): +class Templates(test.TestCase): textile_content = """Paragraph 1 @@ -37,6 +39,13 @@ class Templates(unittest.TestCase): .. _link: http://www.example.com/""" + def setUp(self): + self.save_warnings_state() + warnings.filterwarnings('ignore', category=DeprecationWarning, module='django.contrib.markup') + + def tearDown(self): + self.restore_warnings_state() + @unittest.skipUnless(textile, 'textile not installed') def test_textile(self): t = Template("{% load markup %}{{ textile_content|textile }}") diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 7de2941122fd9..9aa602f41664d 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta import shutil import string import tempfile @@ -302,11 +302,11 @@ def test_exists_searches_cache_first(self): self.assertTrue(self.session.exists(self.session.session_key)) def test_load_overlong_key(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + # Some backends might issue a warning + with warnings.catch_warnings(): + warnings.simplefilter("ignore") self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) - self.assertEqual(len(w), 1) @override_settings(USE_TZ=True) @@ -352,11 +352,11 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase): backend = CacheSession def test_load_overlong_key(self): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + # Some backends might issue a warning + with warnings.catch_warnings(): + warnings.simplefilter("ignore") self.session._session_key = (string.ascii_letters + string.digits) * 20 self.assertEqual(self.session.load(), {}) - self.assertEqual(len(w), 1) class SessionMiddlewareTests(unittest.TestCase): diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index e445d07a61331..7a32a3dac7499 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -223,18 +223,17 @@ def __call__(self, environ, start_response): set_script_prefix(base.get_script_name(environ)) signals.request_started.send(sender=self.__class__) try: - try: - request = self.request_class(environ) - except UnicodeDecodeError: - logger.warning('Bad Request (UnicodeDecodeError)', - exc_info=sys.exc_info(), - extra={ - 'status_code': 400, - } - ) - response = http.HttpResponseBadRequest() - else: - response = self.get_response(request) + request = self.request_class(environ) + except UnicodeDecodeError: + logger.warning('Bad Request (UnicodeDecodeError)', + exc_info=sys.exc_info(), + extra={ + 'status_code': 400, + } + ) + response = http.HttpResponseBadRequest() + else: + response = self.get_response(request) finally: signals.request_finished.send(sender=self.__class__) diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 98f75e0310ca1..b40570efc9e0e 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -5,6 +5,7 @@ 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 @@ -105,7 +106,7 @@ def get_commands(): try: from django.conf import settings apps = settings.INSTALLED_APPS - except (AttributeError, EnvironmentError, ImportError): + except (AttributeError, ImproperlyConfigured): apps = [] # Find and load the management module for each installed app. diff --git a/django/db/models/constants.py b/django/db/models/constants.py new file mode 100644 index 0000000000000..629497eb3d685 --- /dev/null +++ b/django/db/models/constants.py @@ -0,0 +1,7 @@ +""" +Constants used across the ORM in general. +""" + +# Separator used to split filter strings apart. +LOOKUP_SEP = '__' + diff --git a/django/db/models/query.py b/django/db/models/query.py index 05c049b31f51c..8bf08b7a9370a 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -8,6 +8,7 @@ from django.core import exceptions from django.db import connections, router, transaction, IntegrityError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import AutoField from django.db.models.query_utils import (Q, select_related_descend, deferred_class_factory, InvalidQuery) @@ -1613,8 +1614,6 @@ def prefetch_related_objects(result_cache, related_lookups): Populates prefetched objects caches for a list of results from a QuerySet """ - from django.db.models.sql.constants import LOOKUP_SEP - if len(result_cache) == 0: return # nothing to do diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index caf2330bd1171..28d240485819e 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -3,9 +3,10 @@ from django.core.exceptions import FieldError from django.db import transaction from django.db.backends.util import truncate_name +from django.db.models.constants import LOOKUP_SEP from django.db.models.query_utils import select_related_descend from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR, - LOOKUP_SEP, GET_ITERATOR_CHUNK_SIZE) + GET_ITERATOR_CHUNK_SIZE) from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.expressions import SQLEvaluator from django.db.models.sql.query import get_order_dir, Query diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py index b9cf2c96fd68d..f750310624078 100644 --- a/django/db/models/sql/constants.py +++ b/django/db/models/sql/constants.py @@ -1,7 +1,13 @@ +""" +Constants specific to the SQL storage portion of the ORM. +""" + from collections import namedtuple import re -# Valid query types (a set is used for speedy lookups). +# Valid query types (a set is used for speedy lookups). These are (currently) +# considered SQL-specific; other storage systems may choose to use different +# lookup types. QUERY_TERMS = set([ 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', @@ -12,9 +18,6 @@ # Larger values are slightly faster at the expense of more storage space. GET_ITERATOR_CHUNK_SIZE = 100 -# Separator used to split filter strings apart. -LOOKUP_SEP = '__' - # Constants to make looking up tuple values clearer. # Join lists (indexes into the tuples that are values in the alias_map # dictionary in the Query class). diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index 1bbf742b5c195..ac8fea6da3e9a 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -1,6 +1,6 @@ from django.core.exceptions import FieldError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import FieldDoesNotExist -from django.db.models.sql.constants import LOOKUP_SEP class SQLEvaluator(object): def __init__(self, expression, query, allow_joins=True): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index a3601af6b32cc..d9981110494d3 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -15,9 +15,12 @@ from django.utils import six from django.db import connections, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.models.constants import LOOKUP_SEP from django.db.models.expressions import ExpressionNode from django.db.models.fields import FieldDoesNotExist from django.db.models.sql import aggregates as base_aggregates_module +from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE, + ORDER_PATTERN, JoinInfo) from django.db.models.sql.constants import (QUERY_TERMS, LOOKUP_SEP, ORDER_DIR, SINGLE, ORDER_PATTERN, JoinInfo) from django.db.models.sql.matching import match_functions diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index 937505b9b0f1c..c6995c6abbfd1 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -3,6 +3,7 @@ """ from django.core.exceptions import FieldError +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import DateField, FieldDoesNotExist from django.db.models.sql.constants import * from django.db.models.sql.datastructures import Date diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py index 154f2246718c6..a824446b7e925 100644 --- a/django/shortcuts/__init__.py +++ b/django/shortcuts/__init__.py @@ -66,23 +66,7 @@ def redirect(to, *args, **kwargs): else: redirect_class = HttpResponseRedirect - # If it's a model, use get_absolute_url() - if hasattr(to, 'get_absolute_url'): - return redirect_class(to.get_absolute_url()) - - # Next try a reverse URL resolution. - try: - return redirect_class(urlresolvers.reverse(to, args=args, kwargs=kwargs)) - except urlresolvers.NoReverseMatch: - # If this is a callable, re-raise. - if callable(to): - raise - # If this doesn't "feel" like a URL, re-raise. - if '/' not in to and '.' not in to: - raise - - # Finally, fall back and assume it's a URL - return redirect_class(to) + return redirect_class(resolve_url(to, *args, **kwargs)) def _get_queryset(klass): """ @@ -128,3 +112,34 @@ def get_list_or_404(klass, *args, **kwargs): raise Http404('No %s matches the given query.' % queryset.model._meta.object_name) return obj_list +def resolve_url(to, *args, **kwargs): + """ + Return a URL appropriate for the arguments passed. + + The arguments could be: + + * A model: the model's `get_absolute_url()` function will be called. + + * A view name, possibly with arguments: `urlresolvers.reverse()` will + be used to reverse-resolve the name. + + * A URL, which will be returned as-is. + + """ + # If it's a model, use get_absolute_url() + if hasattr(to, 'get_absolute_url'): + return to.get_absolute_url() + + # Next try a reverse URL resolution. + try: + return urlresolvers.reverse(to, args=args, kwargs=kwargs) + except urlresolvers.NoReverseMatch: + # If this is a callable, re-raise. + if callable(to): + raise + # If this doesn't "feel" like a URL, re-raise. + if '/' not in to and '.' not in to: + raise + + # Finally, fall back and assume it's a URL + return to diff --git a/django/utils/html_parser.py b/django/utils/html_parser.py index d7311f253bbea..6ccb665249cfc 100644 --- a/django/utils/html_parser.py +++ b/django/utils/html_parser.py @@ -5,8 +5,7 @@ current_version = sys.version_info use_workaround = ( - (current_version < (2, 6, 8)) or - (current_version >= (2, 7) and current_version < (2, 7, 3)) or + (current_version < (2, 7, 3)) or (current_version >= (3, 0) and current_version < (3, 2, 3)) ) diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index d44246f0b7e4d..52e13a4533af4 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -372,6 +372,9 @@ def get_dated_queryset(self, ordering=None, **lookup): return qs def get_date_list_period(self): + """ + Get the aggregation period for the list of dates: 'year', 'month', or 'day'. + """ return self.date_list_period def get_date_list(self, queryset, date_type=None): diff --git a/docs/faq/models.txt b/docs/faq/models.txt index 4a83aa9f2cc59..69965b66e1895 100644 --- a/docs/faq/models.txt +++ b/docs/faq/models.txt @@ -11,7 +11,7 @@ Then, just do this:: >>> from django.db import connection >>> connection.queries - [{'sql': 'SELECT polls_polls.id,polls_polls.question,polls_polls.pub_date FROM polls_polls', + [{'sql': 'SELECT polls_polls.id, polls_polls.question, polls_polls.pub_date FROM polls_polls', 'time': '0.002'}] ``connection.queries`` is only available if :setting:`DEBUG` is ``True``. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 9359c82e4670b..4add751912680 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -264,6 +264,9 @@ these changes. in 1.4. The backward compatibility will be removed -- ``HttpRequest.raw_post_data`` will no longer work. +* ``django.contrib.markup`` will be removed following an accelerated + deprecation. + 1.7 --- diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 49233fb8a76d9..4d5a86f82b9d7 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -31,7 +31,7 @@ the file ``mysite/news/models.py``:: return self.full_name class Article(models.Model): - pub_date = models.DateTimeField() + pub_date = models.DateField() headline = models.CharField(max_length=200) content = models.TextField() reporter = models.ForeignKey(Reporter) @@ -96,8 +96,8 @@ access your data. The API is created on the fly, no code generation necessary:: DoesNotExist: Reporter matching query does not exist. Lookup parameters were {'id': 2} # Create an article. - >>> from datetime import datetime - >>> a = Article(pub_date=datetime.now(), headline='Django is cool', + >>> from datetime import date + >>> a = Article(pub_date=date.today(), headline='Django is cool', ... content='Yeah.', reporter=r) >>> a.save() @@ -140,7 +140,7 @@ as registering your model in the admin site:: from django.db import models class Article(models.Model): - pub_date = models.DateTimeField() + pub_date = models.DateField() headline = models.CharField(max_length=200) content = models.TextField() reporter = models.ForeignKey(Reporter) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 3f82b44f46190..cc9aa852f156e 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -8,6 +8,11 @@ themselves or inherited from. They may not provide all the capabilities required for projects, in which case there are Mixins and Generic class-based views. +Many of Django's built-in class-based views inherit from other class-based +views or various mixins. Because this inheritence chain is very important, the +ancestor classes are documented under the section title of **Ancestors (MRO)**. +MRO is an acronym for Method Resolution Order. + View ---- @@ -20,6 +25,7 @@ View 1. :meth:`dispatch()` 2. :meth:`http_method_not_allowed()` + 3. :meth:`options()` **Example views.py**:: @@ -41,8 +47,20 @@ View url(r'^mine/$', MyView.as_view(), name='my-view'), ) + **Attributes** + + .. attribute:: http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + + The default list of HTTP method names that this view will accept. + **Methods** + .. classmethod:: as_view(**initkwargs) + + Returns a callable view that takes a request and returns a response:: + + response = MyView.as_view()(request) + .. method:: dispatch(request, *args, **kwargs) The ``view`` part of the view -- the method that accepts a ``request`` @@ -53,6 +71,11 @@ View delegated to :meth:`~View.get()`, a ``POST`` to :meth:`~View.post()`, and so on. + By default, a ``HEAD`` request will be delegated to :meth:`~View.get()`. + If you need to handle ``HEAD`` requests in a different way than ``GET``, + you can override the :meth:`~View.head()` method. See + :ref:`supporting-other-http-methods` for an example. + The default implementation also sets ``request``, ``args`` and ``kwargs`` as instance variables, so any method on the view can know the full details of the request that was made to invoke the view. @@ -62,14 +85,13 @@ View If the view was called with a HTTP method it doesn't support, this method is called instead. - The default implementation returns ``HttpResponseNotAllowed`` with list - of allowed methods in plain text. + The default implementation returns ``HttpResponseNotAllowed`` with a + list of allowed methods in plain text. - .. note:: + .. method:: options(request, *args, **kwargs) - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. + Handles responding to requests for the OPTIONS HTTP verb. Returns a + list of the allowed HTTP method names for the view. TemplateView ------------ @@ -81,6 +103,8 @@ TemplateView **Ancestors (MRO)** + This view inherits methods and attributes from the following views: + * :class:`django.views.generic.base.TemplateView` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.base.View` @@ -116,28 +140,11 @@ TemplateView url(r'^$', HomePageView.as_view(), name='home'), ) - **Methods and Attributes** - - .. attribute:: template_name - - The full name of a template to use. - - .. method:: get_context_data(**kwargs) - - Return a context data dictionary consisting of the contents of - ``kwargs`` stored in the context variable ``params``. - **Context** * ``params``: The dictionary of keyword arguments captured from the URL pattern that served the view. - .. note:: - - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. - RedirectView ------------ @@ -156,6 +163,8 @@ RedirectView **Ancestors (MRO)** + This view inherits methods and attributes from the following view: + * :class:`django.views.generic.base.View` **Method Flowchart** @@ -194,7 +203,7 @@ RedirectView url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'), ) - **Methods and Attributes** + **Attributes** .. attribute:: url @@ -215,6 +224,8 @@ RedirectView then the query string is discarded. By default, ``query_string`` is ``False``. + **Methods** + .. method:: get_redirect_url(**kwargs) Constructs the target URL for redirection. @@ -225,9 +236,3 @@ RedirectView :attr:`~RedirectView.query_string`. Subclasses may implement any behavior they wish, as long as the method returns a redirect-ready URL string. - - .. note:: - - Documentation on class-based views is a work in progress. As yet, only the - methods defined directly on the class are documented here, not methods - defined on superclasses. diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 12776cbb94520..64b269f514733 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -2,13 +2,15 @@ Generic date views ================== -Date-based generic views (in the module :mod:`django.views.generic.dates`) -are views for displaying drilldown pages for date-based data. +.. module:: django.views.generic.dates + +Date-based generic views, provided in :mod:`django.views.generic.dates`, are +views for displaying drilldown pages for date-based data. ArchiveIndexView ---------------- -.. class:: django.views.generic.dates.ArchiveIndexView +.. class:: ArchiveIndexView A top-level index page showing the "latest" objects, by date. Objects with a date in the *future* are not included unless you set ``allow_future`` to @@ -36,7 +38,7 @@ ArchiveIndexView YearArchiveView --------------- -.. class:: django.views.generic.dates.YearArchiveView +.. class:: YearArchiveView A yearly archive page showing all available months in a given year. Objects with a date in the *future* are not displayed unless you set @@ -58,13 +60,15 @@ YearArchiveView A boolean specifying whether to retrieve the full list of objects for this year and pass those to the template. If ``True``, the list of - objects will be made available to the context. By default, this is + objects will be made available to the context. If ``False``, the + ``None`` queryset will be used as the object list. By default, this is ``False``. .. method:: get_make_object_list() - Determine if an object list will be returned as part of the context. If - ``False``, the ``None`` queryset will be used as the object list. + Determine if an object list will be returned as part of the context. + Returns :attr:`~YearArchiveView.make_object_list` by default. + **Context** @@ -80,16 +84,18 @@ YearArchiveView :class:`datetime.datetime` objects, in ascending order. - * ``year``: A :class:`datetime.date` object + * ``year``: A :class:`~datetime.date` object representing the given year. - * ``next_year``: A :class:`datetime.date` object - representing the first day of the next year. If the next year is in the - future, this will be ``None``. + * ``next_year``: A :class:`~datetime.date` object + representing the first day of the next year, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_year``: A :class:`datetime.date` object - representing the first day of the previous year. Unlike ``next_year``, - this will never be ``None``. + * ``previous_year``: A :class:`~datetime.date` object + representing the first day of the previous year, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -98,7 +104,7 @@ YearArchiveView MonthArchiveView ---------------- -.. class:: django.views.generic.dates.MonthArchiveView +.. class:: MonthArchiveView A monthly archive page showing all objects in a given month. Objects with a date in the *future* are not displayed unless you set ``allow_future`` to @@ -131,16 +137,18 @@ MonthArchiveView :class:`datetime.datetime` objects, in ascending order. - * ``month``: A :class:`datetime.date` object + * ``month``: A :class:`~datetime.date` object representing the given month. - * ``next_month``: A :class:`datetime.date` object - representing the first day of the next month. If the next month is in the - future, this will be ``None``. + * ``next_month``: A :class:`~datetime.date` object + representing the first day of the next month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_month``: A :class:`datetime.date` object - representing the first day of the previous month. Unlike ``next_month``, - this will never be ``None``. + * ``previous_month``: A :class:`~datetime.date` object + representing the first day of the previous month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -149,7 +157,7 @@ MonthArchiveView WeekArchiveView --------------- -.. class:: django.views.generic.dates.WeekArchiveView +.. class:: WeekArchiveView A weekly archive page showing all objects in a given week. Objects with a date in the *future* are not displayed unless you set ``allow_future`` to @@ -175,16 +183,18 @@ WeekArchiveView :class:`~django.views.generic.dates.BaseDateListView`), the template's context will be: - * ``week``: A :class:`datetime.date` object + * ``week``: A :class:`~datetime.date` object representing the first day of the given week. - * ``next_week``: A :class:`datetime.date` object - representing the first day of the next week. If the next week is in the - future, this will be ``None``. + * ``next_week``: A :class:`~datetime.date` object + representing the first day of the next week, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_week``: A :class:`datetime.date` object - representing the first day of the previous week. Unlike ``next_week``, - this will never be ``None``. + * ``previous_week``: A :class:`~datetime.date` object + representing the first day of the previous week, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -193,7 +203,7 @@ WeekArchiveView DayArchiveView -------------- -.. class:: django.views.generic.dates.DayArchiveView +.. class:: DayArchiveView A day archive page showing all objects in a given day. Days in the future throw a 404 error, regardless of whether any objects exist for future days, @@ -220,24 +230,28 @@ DayArchiveView :class:`~django.views.generic.dates.BaseDateListView`), the template's context will be: - * ``day``: A :class:`datetime.date` object + * ``day``: A :class:`~datetime.date` object representing the given day. - * ``next_day``: A :class:`datetime.date` object - representing the next day. If the next day is in the future, this will be - ``None``. + * ``next_day``: A :class:`~datetime.date` object + representing the next day, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_day``: A :class:`datetime.date` object - representing the previous day. Unlike ``next_day``, this will never be - ``None``. + * ``previous_day``: A :class:`~datetime.date` object + representing the previous day, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``next_month``: A :class:`datetime.date` object - representing the first day of the next month. If the next month is in the - future, this will be ``None``. + * ``next_month``: A :class:`~datetime.date` object + representing the first day of the next month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. - * ``previous_month``: A :class:`datetime.date` object - representing the first day of the previous month. Unlike ``next_month``, - this will never be ``None``. + * ``previous_month``: A :class:`~datetime.date` object + representing the first day of the previous month, according to + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. **Notes** @@ -246,7 +260,7 @@ DayArchiveView TodayArchiveView ---------------- -.. class:: django.views.generic.dates.TodayArchiveView +.. class:: TodayArchiveView A day archive page showing all objects for *today*. This is exactly the same as :class:`django.views.generic.dates.DayArchiveView`, except today's @@ -271,7 +285,7 @@ TodayArchiveView DateDetailView -------------- -.. class:: django.views.generic.dates.DateDetailView +.. class:: DateDetailView A page representing an individual object. If the object has a date value in the future, the view will throw a 404 error by default, unless you set @@ -293,6 +307,22 @@ DateDetailView .. note:: - All of the generic views listed above have matching Base* views that only - differ in that the they do not include the - :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. + All of the generic views listed above have matching ``Base`` views that + only differ in that the they do not include the + :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`: + + .. class:: BaseArchiveIndexView + + .. class:: BaseYearArchiveView + + .. class:: BaseMonthArchiveView + + .. class:: BaseWeekArchiveView + + .. class:: BaseDayArchiveView + + .. class:: BaseTodayArchiveView + + .. class:: BaseDateDetailView + + diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index ef3bc179eed6e..12603ff0df283 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -15,6 +15,8 @@ DetailView **Ancestors (MRO)** + This view inherits methods and attributes from the following views: + * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.detail.BaseDetailView` @@ -71,7 +73,9 @@ ListView objects (usually, but not necessarily a queryset) that the view is operating upon. - **Mixins** + **Ancestors (MRO)** + + This view inherits methods and attributes from the following views: * :class:`django.views.generic.list.ListView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` @@ -90,3 +94,54 @@ ListView 6. :meth:`get_context_data()` 7. :meth:`get()` 8. :meth:`render_to_response()` + + + **Example views.py**:: + + from django.views.generic.list import ListView + from django.utils import timezone + + from articles.models import Article + + class ArticleListView(ListView): + + model = Article + + def get_context_data(self, **kwargs): + context = super(ArticleListView, self).get_context_data(**kwargs) + context['now'] = timezone.now() + return context + + **Example urls.py**:: + + from django.conf.urls import patterns, url + + from article.views import ArticleListView + + urlpatterns = patterns('', + url(r'^$', ArticleListView.as_view(), name='article-list'), + ) + +.. class:: django.views.generic.list.BaseListView + + A base view for displaying a list of objects. It is not intended to be used + directly, but rather as a parent class of the + :class:`django.views.generic.list.ListView` or other views representing + lists of objects. + + **Ancestors (MRO)** + + This view inherits methods and attributes from the following views: + + * :class:`django.views.generic.list.MultipleObjectMixin` + * :class:`django.views.generic.base.View` + + **Methods** + + .. method:: get(request, *args, **kwargs) + + Adds :attr:`object_list` to the context. If + :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` + is True then display an empty list. If + :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is + False then raise a 404 error. diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt index f0e7bbc6c19ab..c4b632604a445 100644 --- a/docs/ref/class-based-views/index.txt +++ b/docs/ref/class-based-views/index.txt @@ -23,7 +23,7 @@ it is safe to store state variables on the instance (i.e., ``self.foo = 3`` is a thread-safe operation). A class-based view is deployed into a URL pattern using the -:meth:`~View.as_view()` classmethod:: +:meth:`~django.views.generic.base.View.as_view()` classmethod:: urlpatterns = patterns('', (r'^view/$', MyView.as_view(size=42)), @@ -37,9 +37,10 @@ A class-based view is deployed into a URL pattern using the is modified, the actions of one user visiting your view could have an effect on subsequent users visiting the same view. -Any argument passed into :meth:`~View.as_view()` will be assigned onto the -instance that is used to service a request. Using the previous example, -this means that every request on ``MyView`` is able to use ``self.size``. +Any argument passed into :meth:`~django.views.generic.base.View.as_view()` will +be assigned onto the instance that is used to service a request. Using the +previous example, this means that every request on ``MyView`` is able to use +``self.size``. Base vs Generic views --------------------- diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 6bf6f10b5d489..01181ebb6c65c 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -2,11 +2,12 @@ Date-based mixins ================= +.. currentmodule:: django.views.generic.dates YearMixin --------- -.. class:: django.views.generic.dates.YearMixin +.. class:: YearMixin A mixin that can be used to retrieve and provide parsing information for a year component of a date. @@ -20,29 +21,45 @@ YearMixin .. attribute:: year - **Optional** The value for the year (as a string). By default, set to + **Optional** The value for the year, as a string. By default, set to ``None``, which means the year will be determined using other means. .. method:: get_year_format() - Returns the :func:`~time.strftime` format to use when parsing the year. Returns - :attr:`YearMixin.year_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + year. Returns :attr:`~YearMixin.year_format` by default. .. method:: get_year() - Returns the year for which this view will display data. Tries the - following sources, in order: + Returns the year for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`YearMixin.year` attribute. - * The value of the `year` argument captured in the URL pattern + * The value of the `year` argument captured in the URL pattern. * The value of the `year` GET query argument. Raises a 404 if no valid year specification can be found. + .. method:: get_next_year(date) + + Returns a date object containing the first day of the year after the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + + .. method:: get_previous_year(date) + + Returns a date object containing the first day of the year before the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + MonthMixin ---------- -.. class:: django.views.generic.dates.MonthMixin +.. class:: MonthMixin A mixin that can be used to retrieve and provide parsing information for a month component of a date. @@ -51,26 +68,26 @@ MonthMixin .. attribute:: month_format - The :func:`~time.strftime` format to use when parsing the month. By default, this is - ``'%b'``. + The :func:`~time.strftime` format to use when parsing the month. By + default, this is ``'%b'``. .. attribute:: month - **Optional** The value for the month (as a string). By default, set to + **Optional** The value for the month, as a string. By default, set to ``None``, which means the month will be determined using other means. .. method:: get_month_format() - Returns the :func:`~time.strftime` format to use when parsing the month. Returns - :attr:`MonthMixin.month_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + month. Returns :attr:`~MonthMixin.month_format` by default. .. method:: get_month() - Returns the month for which this view will display data. Tries the - following sources, in order: + Returns the month for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`MonthMixin.month` attribute. - * The value of the `month` argument captured in the URL pattern + * The value of the `month` argument captured in the URL pattern. * The value of the `month` GET query argument. Raises a 404 if no valid month specification can be found. @@ -78,20 +95,23 @@ MonthMixin .. method:: get_next_month(date) Returns a date object containing the first day of the month after the - date provided. Returns ``None`` if mixed with a view that sets - ``allow_future = False``, and the next month is in the future. If - ``allow_empty = False``, returns the next month that contains data. + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. .. method:: get_prev_month(date) Returns a date object containing the first day of the month before the - date provided. If ``allow_empty = False``, returns the previous month - that contained data. + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. DayMixin -------- -.. class:: django.views.generic.dates.DayMixin +.. class:: DayMixin A mixin that can be used to retrieve and provide parsing information for a day component of a date. @@ -100,46 +120,50 @@ DayMixin .. attribute:: day_format - The :func:`~time.strftime` format to use when parsing the day. By default, this is - ``'%d'``. + The :func:`~time.strftime` format to use when parsing the day. By + default, this is ``'%d'``. .. attribute:: day - **Optional** The value for the day (as a string). By default, set to + **Optional** The value for the day, as a string. By default, set to ``None``, which means the day will be determined using other means. .. method:: get_day_format() - Returns the :func:`~time.strftime` format to use when parsing the day. Returns - :attr:`DayMixin.day_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the day. + Returns :attr:`~DayMixin.day_format` by default. .. method:: get_day() - Returns the day for which this view will display data. Tries the - following sources, in order: + Returns the day for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`DayMixin.day` attribute. - * The value of the `day` argument captured in the URL pattern + * The value of the `day` argument captured in the URL pattern. * The value of the `day` GET query argument. Raises a 404 if no valid day specification can be found. .. method:: get_next_day(date) - Returns a date object containing the next day after the date provided. - Returns ``None`` if mixed with a view that sets ``allow_future = False``, - and the next day is in the future. If ``allow_empty = False``, returns - the next day that contains data. + Returns a date object containing the next valid day after the date + provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. .. method:: get_prev_day(date) - Returns a date object containing the previous day. If - ``allow_empty = False``, returns the previous day that contained data. + Returns a date object containing the previous valid day. This function + can also return ``None`` or raise an :class:`~django.http.Http404` + exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. WeekMixin --------- -.. class:: django.views.generic.dates.WeekMixin +.. class:: WeekMixin A mixin that can be used to retrieve and provide parsing information for a week component of a date. @@ -148,23 +172,24 @@ WeekMixin .. attribute:: week_format - The :func:`~time.strftime` format to use when parsing the week. By default, this is - ``'%U'``. + The :func:`~time.strftime` format to use when parsing the week. By + default, this is ``'%U'``, which means the week starts on Sunday. Set + it to ``'%W'`` if your week starts on Monday. .. attribute:: week - **Optional** The value for the week (as a string). By default, set to + **Optional** The value for the week, as a string. By default, set to ``None``, which means the week will be determined using other means. .. method:: get_week_format() - Returns the :func:`~time.strftime` format to use when parsing the week. Returns - :attr:`WeekMixin.week_format` by default. + Returns the :func:`~time.strftime` format to use when parsing the + week. Returns :attr:`~WeekMixin.week_format` by default. .. method:: get_week() - Returns the week for which this view will display data. Tries the - following sources, in order: + Returns the week for which this view will display data, as a string. + Tries the following sources, in order: * The value of the :attr:`WeekMixin.week` attribute. * The value of the `week` argument captured in the URL pattern @@ -172,11 +197,26 @@ WeekMixin Raises a 404 if no valid week specification can be found. + .. method:: get_next_week(date) + + Returns a date object containing the first day of the week after the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. + + .. method:: get_prev_week(date) + + Returns a date object containing the first day of the week before the + date provided. This function can also return ``None`` or raise an + :class:`~django.http.Http404` exception, depending on the values of + :attr:`~BaseDateListView.allow_empty` and + :attr:`~DateMixin.allow_future`. DateMixin --------- -.. class:: django.views.generic.dates.DateMixin +.. class:: DateMixin A mixin class providing common behavior for all date-based views. @@ -186,7 +226,7 @@ DateMixin The name of the ``DateField`` or ``DateTimeField`` in the ``QuerySet``'s model that the date-based archive should use to - determine the objects on the page. + determine the list of objects to display on the page. When :doc:`time zone support ` is enabled and ``date_field`` is a ``DateTimeField``, dates are assumed to be in the @@ -210,26 +250,26 @@ DateMixin .. method:: get_date_field() Returns the name of the field that contains the date data that this - view will operate on. Returns :attr:`DateMixin.date_field` by default. + view will operate on. Returns :attr:`~DateMixin.date_field` by default. .. method:: get_allow_future() Determine whether to include "future" objects on this page, where "future" means objects in which the field specified in ``date_field`` is greater than the current date/time. Returns - :attr:`DateMixin.allow_future` by default. + :attr:`~DateMixin.allow_future` by default. BaseDateListView ---------------- -.. class:: django.views.generic.dates.BaseDateListView +.. class:: BaseDateListView A base class that provides common behavior for all date-based views. There won't normally be a reason to instantiate :class:`~django.views.generic.dates.BaseDateListView`; instantiate one of the subclasses instead. - While this view (and it's subclasses) are executing, ``self.object_list`` + While this view (and its subclasses) are executing, ``self.object_list`` will contain the list of objects that the view is operating upon, and ``self.date_list`` will contain the list of dates for which data is available. @@ -245,10 +285,18 @@ BaseDateListView A boolean specifying whether to display the page if no objects are available. If this is ``True`` and no objects are available, the view - will display an empty page instead of raising a 404. By default, this - is ``False``. + will display an empty page instead of raising a 404. + + This is identical to :attr:`MultipleObjectMixin.allow_empty`, except + for the default value, which is ``False``. + + .. attribute:: date_list_period - .. method:: get_dated_items(): + **Optional** A string defining the aggregation period for + ``date_list``. It must be one of ``'year'`` (default), ``'month'``, or + ``'day'``. + + .. method:: get_dated_items() Returns a 3-tuple containing (``date_list``, ``object_list``, ``extra_context``). @@ -265,10 +313,17 @@ BaseDateListView ``lookup``. Enforces any restrictions on the queryset, such as ``allow_empty`` and ``allow_future``. - .. method:: get_date_list(queryset, date_type) + .. method:: get_date_list_period() + + Returns the aggregation period for ``date_list``. Returns + :attr:`~BaseDateListView.date_list_period` by default. + + .. method:: get_date_list(queryset, date_type=None) - Returns the list of dates of type ``date_type`` for which - ``queryset`` contains entries. For example, ``get_date_list(qs, - 'year')`` will return the list of years for which ``qs`` has entries. - See :meth:`~django.db.models.query.QuerySet.dates()` for the - ways that the ``date_type`` argument can be used. + Returns the list of dates of type ``date_type`` for which ``queryset`` + contains entries. For example, ``get_date_list(qs, 'year')`` will + return the list of years for which ``qs`` has entries. If + ``date_type`` isn't provided, the result of + :meth:`BaseDateListView.get_date_list_period` is used. See + :meth:`~django.db.models.query.QuerySet.dates()` for the ways that the + ``date_type`` argument can be used. diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index 8bc613b887971..cdb743fcbd1b6 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -86,7 +86,8 @@ MultipleObjectMixin .. method:: get_queryset() - Returns the queryset that represents the data this view will display. + Get the list of items for this view. This must be an iterable and may + be a queryset (in which queryset-specific behavior will be enabled). .. method:: paginate_queryset(queryset, page_size) diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 61fc945cd3c4f..d2f0df241e5d9 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -9,16 +9,17 @@ ContextMixin .. versionadded:: 1.5 - **classpath** - - ``django.views.generic.base.ContextMixin`` - **Methods** .. method:: get_context_data(**kwargs) Returns a dictionary representing the template context. The keyword - arguments provided will make up the returned context. + arguments provided will make up the returned context. Example usage:: + + def get_context_data(self, **kwargs): + context = super(RandomNumberView, self).get_context_data(**kwargs) + context['number'] = random.randrange(1, 100) + return context The template context of all class-based generic views include a ``view`` variable that points to the ``View`` instance. @@ -42,7 +43,13 @@ TemplateResponseMixin suitable context. The template to use is configurable and can be further customized by subclasses. - **Methods and Attributes** + **Attributes** + + .. attribute:: template_name + + The full name of a template to use as defined by a string. Not defining + a template_name will raise a + :class:`django.core.exceptions.ImproperlyConfigured` exception. .. attribute:: response_class @@ -57,12 +64,14 @@ TemplateResponseMixin instantiation, create a ``TemplateResponse`` subclass and assign it to ``response_class``. + **Methods** + .. method:: render_to_response(context, **response_kwargs) Returns a ``self.response_class`` instance. - If any keyword arguments are provided, they will be - passed to the constructor of the response class. + If any keyword arguments are provided, they will be passed to the + constructor of the response class. Calls :meth:`~TemplateResponseMixin.get_template_names()` to obtain the list of template names that will be searched looking for an existent diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index 4f4b326cb2a56..f03c7fda0d5f1 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -32,11 +32,11 @@ A simple example is the best illustration of this. Suppose we have the following model, which would represent entries in a Weblog:: from django.db import models - + class Entry(models.Model): title = models.CharField(maxlength=250) body = models.TextField() - pub_date = models.DateTimeField() + pub_date = models.DateField() enable_comments = models.BooleanField() Now, suppose that we want the following steps to be applied whenever a @@ -55,11 +55,11 @@ Accomplishing this is fairly straightforward and requires very little code:: from django.contrib.comments.moderation import CommentModerator, moderator - + class EntryModerator(CommentModerator): email_notification = True enable_field = 'enable_comments' - + moderator.register(Entry, EntryModerator) The :class:`CommentModerator` class pre-defines a number of useful moderation diff --git a/docs/ref/contrib/markup.txt b/docs/ref/contrib/markup.txt index 8f3e0a95f9041..9215c64f93297 100644 --- a/docs/ref/contrib/markup.txt +++ b/docs/ref/contrib/markup.txt @@ -5,6 +5,9 @@ django.contrib.markup .. module:: django.contrib.markup :synopsis: A collection of template filters that implement common markup languages. +.. deprecated:: 1.5 + This module has been deprecated. + Django provides template filters that implement the following markup languages: diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 275c696230f8c..8b3c31f0294f9 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -195,6 +195,14 @@ support tablespaces for indexes, this option is ignored. The default value for the field. This can be a value or a callable object. If callable it will be called every time a new object is created. +The default cannot be a mutable object (model instance, list, set, etc.), as a +reference to the same instance of that object would be used as the default +value in all new model instances. Instead, wrap the desired default in a +callable. For example, if you had a custom ``JSONField`` and wanted to specify +a dictionary as the default, use a ``lambda`` as follows:: + + contact_info = JSONField("ContactInfo", default=lambda:{"email": "to1@example.com"}) + ``editable`` ------------ @@ -983,10 +991,10 @@ define the details of how the relation works. this with functions from the Python ``datetime`` module to limit choices of objects by date. For example:: - limit_choices_to = {'pub_date__lte': datetime.now} + limit_choices_to = {'pub_date__lte': datetime.date.today} only allows the choice of related objects with a ``pub_date`` before the - current date/time to be chosen. + current date to be chosen. Instead of a dictionary this can also be a :class:`~django.db.models.Q` object for more :ref:`complex queries `. However, diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index 472ac964578f8..2fdc87df8c7ef 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -135,7 +135,7 @@ access to more than a single field:: raise ValidationError('Draft entries may not have a publication date.') # Set the pub_date for published items if it hasn't been set already. if self.status == 'published' and self.pub_date is None: - self.pub_date = datetime.datetime.now() + self.pub_date = datetime.date.today() Any :exc:`~django.core.exceptions.ValidationError` exceptions raised by ``Model.clean()`` will be stored in a special key error dictionary key, diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 4f5f8858b53c4..80b3158f01dec 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -31,6 +31,9 @@ You can evaluate a ``QuerySet`` in the following ways: for e in Entry.objects.all(): print(e.headline) + Note: Don't use this if all you want to do is determine if at least one + result exists. It's more efficient to use :meth:`~QuerySet.exists`. + * **Slicing.** As explained in :ref:`limiting-querysets`, a ``QuerySet`` can be sliced, using Python's array-slicing syntax. Slicing an unevaluated ``QuerySet`` usually returns another unevaluated ``QuerySet``, but Django @@ -75,7 +78,7 @@ You can evaluate a ``QuerySet`` in the following ways: Note: *Don't* use this if all you want to do is determine if at least one result exists, and don't need the actual objects. It's more efficient to - use :meth:`exists() ` (see below). + use :meth:`~QuerySet.exists` (see below). .. _pickling QuerySets: @@ -1523,9 +1526,40 @@ exists Returns ``True`` if the :class:`.QuerySet` contains any results, and ``False`` if not. This tries to perform the query in the simplest and fastest way -possible, but it *does* execute nearly the same query. This means that calling -:meth:`.QuerySet.exists` is faster than ``bool(some_query_set)``, but not by -a large degree. If ``some_query_set`` has not yet been evaluated, but you know +possible, but it *does* execute nearly the same query as a normal +:class:`.QuerySet` query. + +:meth:`~.QuerySet.exists` is useful for searches relating to both +object membership in a :class:`.QuerySet` and to the existence of any objects in +a :class:`.QuerySet`, particularly in the context of a large :class:`.QuerySet`. + +The most efficient method of finding whether a model with a unique field +(e.g. ``primary_key``) is a member of a :class:`.QuerySet` is:: + + entry = Entry.objects.get(pk=123) + if some_query_set.filter(pk=entry.pk).exists(): + print("Entry contained in queryset") + +Which will be faster than the following which requires evaluating and iterating +through the entire queryset:: + + if entry in some_query_set: + print("Entry contained in QuerySet") + +And to find whether a queryset contains any items:: + + if some_query_set.exists(): + print("There is at least one object in some_query_set") + +Which will be faster than:: + + if some_query_set: + print("There is at least one object in some_query_set") + +... but not by a large degree (hence needing a large queryset for efficiency +gains). + +Additionally, if a ``some_query_set`` has not yet been evaluated, but you know that it will be at some point, then using ``some_query_set.exists()`` will do more overall work (one query for the existence check plus an extra one to later retrieve the results) than simply using ``bool(some_query_set)``, which @@ -1945,6 +1979,17 @@ SQL equivalent:: You can use ``range`` anywhere you can use ``BETWEEN`` in SQL — for dates, numbers and even characters. +.. warning:: + + Filtering a ``DateTimeField`` with dates won't include items on the last + day, because the bounds are interpreted as "0am on the given date". If + ``pub_date`` was a ``DateTimeField``, the above expression would be turned + into this SQL:: + + SELECT ... WHERE pub_date BETWEEN '2005-01-01 00:00:00' and '2005-03-31 00:00:00'; + + Generally speaking, you can't mix dates and datetimes. + .. fieldlookup:: year year @@ -1958,7 +2003,7 @@ Example:: SQL equivalent:: - SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31 23:59:59.999999'; + SELECT ... WHERE pub_date BETWEEN '2005-01-01' AND '2005-12-31'; (The exact SQL syntax varies for each database engine.) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 4729a2b6f19e9..f44313856913c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1304,25 +1304,13 @@ The URL where requests are redirected after login when the This is used by the :func:`~django.contrib.auth.decorators.login_required` decorator, for example. -.. _`note on LOGIN_REDIRECT_URL setting`: - -.. note:: - You can use :func:`~django.core.urlresolvers.reverse_lazy` to reference - URLs by their name instead of providing a hardcoded value. Assuming a - ``urls.py`` with an URLpattern named ``home``:: - - urlpatterns = patterns('', - url('^welcome/$', 'test_app.views.home', name='home'), - ) - - You can use :func:`~django.core.urlresolvers.reverse_lazy` like this:: - - from django.core.urlresolvers import reverse_lazy - - LOGIN_REDIRECT_URL = reverse_lazy('home') +.. versionchanged:: 1.5 - This also works fine with localized URLs using - :func:`~django.conf.urls.i18n.i18n_patterns`. +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. .. setting:: LOGIN_URL @@ -1334,8 +1322,13 @@ Default: ``'/accounts/login/'`` The URL where requests are redirected for login, especially when using the :func:`~django.contrib.auth.decorators.login_required` decorator. -.. note:: - See the `note on LOGIN_REDIRECT_URL setting`_ +.. versionchanged:: 1.5 + +This setting now also accepts view function names and +:ref:`named URL patterns ` which can be used to reduce +configuration duplication since you no longer have to define the URL in two +places (``settings`` and URLconf). +For backward compatibility reasons the default remains unchanged. .. setting:: LOGOUT_URL diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 5728d8559a8ca..6420239f47658 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -121,6 +121,12 @@ Django 1.5 also includes several smaller improvements worth noting: argument. By default the batch_size is unlimited except for SQLite where single batch is limited so that 999 parameters per query isn't exceeded. +* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also + accept view function names and + :ref:`named URL patterns `. This allows you to reduce + configuration duplication. More information can be found in the + :func:`~django.contrib.auth.decorators.login_required` documentation. + Backwards incompatible changes in 1.5 ===================================== @@ -358,3 +364,11 @@ the built-in :func:`itertools.product` instead. The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. Define a ``__str__`` method and apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. + +``django.utils.markup`` +~~~~~~~~~~~~~~~~~~~~~~~ + +The markup contrib module has been deprecated and will follow an accelerated +deprecation schedule. Direct use of python markup libraries or 3rd party tag +libraries is preferred to Django maintaining this functionality in the +framework. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index fa55a4d2062ee..2329d1effa960 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -14,6 +14,7 @@ up to and including the new version. Final releases ============== +.. _development_release_notes: 1.5 release ----------- diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index f8dbbb65a6a21..c45e4bbaf7010 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -947,6 +947,13 @@ The login_required decorator (r'^accounts/login/$', 'django.contrib.auth.views.login'), + .. versionchanged:: 1.5 + + As of version 1.5 :setting:`settings.LOGIN_URL ` now also accepts + view function names and :ref:`named URL patterns `. + This allows you to freely remap your login view within your URLconf + without having to update the setting. + .. function:: views.login(request, [template_name, redirect_field_name, authentication_form]) **URL name:** ``login`` diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 6637bd5fcb33d..2d3e00ab4cd74 100644 --- a/docs/topics/class-based-views/index.txt +++ b/docs/topics/class-based-views/index.txt @@ -84,6 +84,50 @@ function-like entry to class-based views:: For more information on how to use the built in generic views, consult the next topic on :doc:`generic class based views`. +.. _supporting-other-http-methods: + +Supporting other HTTP methods +----------------------------- + +Suppose somebody wants to access our book library over HTTP using the views +as an API. The API client would connect every now and then and download book +data for the books published since last visit. But if no new books appeared +since then, it is a waste of CPU time and bandwidth to fetch the books from the +database, render a full response and send it to the client. It might be +preferable to ask the API when the most recent book was published. + +We map the URL to book list view in the URLconf:: + + from django.conf.urls import patterns + from books.views import BookListView + + urlpatterns = patterns('', + (r'^books/$', BookListView.as_view()), + ) + +And the view:: + + from django.http import HttpResponse + from django.views.generic import ListView + from books.models import Book + + class BookListView(ListView): + model = Book + + def head(self, *args, **kwargs): + last_book = self.get_queryset().latest('publication_date') + response = HttpResponse('') + # RFC 1123 date format + response['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + return response + +If the view is accessed from a ``GET`` request, a plain-and-simple object +list is returned in the response (using ``book_list.html`` template). But if +the client issues a ``HEAD`` request, the response has an empty body and +the ``Last-Modified`` header indicates when the most recent book was published. +Based on this information, the client may or may not download the full object +list. + Decorating class-based views ============================ diff --git a/docs/topics/db/examples/many_to_one.txt b/docs/topics/db/examples/many_to_one.txt index 0a9978b8d1f8b..c869362d16e98 100644 --- a/docs/topics/db/examples/many_to_one.txt +++ b/docs/topics/db/examples/many_to_one.txt @@ -42,8 +42,8 @@ Create a few Reporters:: Create an Article:: - >>> from datetime import datetime - >>> a = Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter=r) + >>> from datetime import date + >>> a = Article(id=None, headline="This is a test", pub_date=date(2005, 7, 27), reporter=r) >>> a.save() >>> a.reporter.id @@ -65,7 +65,7 @@ database, which always returns unicode strings):: Create an Article via the Reporter object:: - >>> new_article = r.article_set.create(headline="John's second story", pub_date=datetime(2005, 7, 29)) + >>> new_article = r.article_set.create(headline="John's second story", pub_date=date(2005, 7, 29)) >>> new_article >>> new_article.reporter @@ -75,7 +75,7 @@ Create an Article via the Reporter object:: Create a new article, and add it to the article set:: - >>> new_article2 = Article(headline="Paul's story", pub_date=datetime(2006, 1, 17)) + >>> new_article2 = Article(headline="Paul's story", pub_date=date(2006, 1, 17)) >>> r.article_set.add(new_article2) >>> new_article2.reporter diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 03a7d3b7cd9b8..d2ff8645a96c2 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -201,73 +201,129 @@ An example write to propagate to the slaves). It also doesn't consider the interaction of transactions with the database utilization strategy. -So - what does this mean in practice? Say you want ``myapp`` to -exist on the ``other`` database, and you want all other models in a -master/slave relationship between the databases ``master``, ``slave1`` and -``slave2``. To implement this, you would need 2 routers:: +So - what does this mean in practice? Let's consider another sample +configuration. This one will have several databases: one for the +``auth`` application, and all other apps using a master/slave setup +with two read slaves. Here are the settings specifying these +databases:: - class MyAppRouter(object): - """A router to control all database operations on models in - the myapp application""" + DATABASES = { + 'auth_db': { + 'NAME': 'auth_db', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'swordfish', + }, + 'master': { + 'NAME': 'master', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'spam', + }, + 'slave1': { + 'NAME': 'slave1', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'eggs', + }, + 'slave2': { + 'NAME': 'slave2', + 'ENGINE': 'django.db.backends.mysql', + 'USER': 'mysql_user', + 'PASSWORD': 'bacon', + }, + } + +Now we'll need to handle routing. First we want a router that knows to +send queries for the ``auth`` app to ``auth_db``:: + class AuthRouter(object): + """ + A router to control all database operations on models in the + auth application. + """ def db_for_read(self, model, **hints): - "Point all operations on myapp models to 'other'" - if model._meta.app_label == 'myapp': - return 'other' + """ + Attempts to read auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' return None def db_for_write(self, model, **hints): - "Point all operations on myapp models to 'other'" - if model._meta.app_label == 'myapp': - return 'other' + """ + Attempts to write auth models go to auth_db. + """ + if model._meta.app_label == 'auth': + return 'auth_db' return None def allow_relation(self, obj1, obj2, **hints): - "Allow any relation if a model in myapp is involved" - if obj1._meta.app_label == 'myapp' or obj2._meta.app_label == 'myapp': - return True + """ + Allow relations if a model in the auth app is involved. + """ + if obj1._meta.app_label == 'auth' or \ + obj2._meta.app_label == 'auth': + return True return None def allow_syncdb(self, db, model): - "Make sure the myapp app only appears on the 'other' db" - if db == 'other': - return model._meta.app_label == 'myapp' - elif model._meta.app_label == 'myapp': + """ + Make sure the auth app only appears in the 'auth_db' + database. + """ + if db == 'auth_db': + return model._meta.app_label == 'auth' + elif model._meta.app_label == 'auth': return False return None - class MasterSlaveRouter(object): - """A router that sets up a simple master/slave configuration""" +And we also want a router that sends all other apps to the +master/slave configuration, and randomly chooses a slave to read +from:: + import random + + class MasterSlaveRouter(object): def db_for_read(self, model, **hints): - "Point all read operations to a random slave" - return random.choice(['slave1','slave2']) + """ + Reads go to a randomly-chosen slave. + """ + return random.choice(['slave1', 'slave2']) def db_for_write(self, model, **hints): - "Point all write operations to the master" + """ + Writes always go to master. + """ return 'master' def allow_relation(self, obj1, obj2, **hints): - "Allow any relation between two objects in the db pool" - db_list = ('master','slave1','slave2') - if obj1._state.db in db_list and obj2._state.db in db_list: + """ + Relations between objects are allowed if both objects are + in the master/slave pool. + """ + db_list = ('master', 'slave1', 'slave2') + if obj1.state.db in db_list and obj2.state.db in db_list: return True return None def allow_syncdb(self, db, model): - "Explicitly put all models on all databases." + """ + All non-auth models end up in this pool. + """ return True -Then, in your settings file, add the following (substituting ``path.to.`` with -the actual python path to the module where you define the routers):: +Finally, in the settings file, we add the following (substituting +``path.to.`` with the actual python path to the module(s) where the +routers are defined):: - DATABASE_ROUTERS = ['path.to.MyAppRouter', 'path.to.MasterSlaveRouter'] + DATABASE_ROUTERS = ['path.to.AuthRouter', 'path.to.MasterSlaveRouter'] The order in which routers are processed is significant. Routers will be queried in the order the are listed in the :setting:`DATABASE_ROUTERS` setting . In this example, the -``MyAppRouter`` is processed before the ``MasterSlaveRouter``, and as a -result, decisions concerning the models in ``myapp`` are processed +``AuthRouter`` is processed before the ``MasterSlaveRouter``, and as a +result, decisions concerning the models in ``auth`` are processed before any other decision is made. If the :setting:`DATABASE_ROUTERS` setting listed the two routers in the other order, ``MasterSlaveRouter.allow_syncdb()`` would be processed first. The @@ -276,11 +332,11 @@ that all models would be available on all databases. With this setup installed, lets run some Django code:: - >>> # This retrieval will be performed on the 'credentials' database + >>> # This retrieval will be performed on the 'auth_db' database >>> fred = User.objects.get(username='fred') >>> fred.first_name = 'Frederick' - >>> # This save will also be directed to 'credentials' + >>> # This save will also be directed to 'auth_db' >>> fred.save() >>> # These retrieval will be randomly allocated to a slave database diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 12e9b447b3c9a..5385b2a72d2d1 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -35,8 +35,8 @@ models, which comprise a Weblog application: blog = models.ForeignKey(Blog) headline = models.CharField(max_length=255) body_text = models.TextField() - pub_date = models.DateTimeField() - mod_date = models.DateTimeField() + pub_date = models.DateField() + mod_date = models.DateField() authors = models.ManyToManyField(Author) n_comments = models.IntegerField() n_pingbacks = models.IntegerField() @@ -233,7 +233,7 @@ refinements together. For example:: >>> Entry.objects.filter( ... headline__startswith='What' ... ).exclude( - ... pub_date__gte=datetime.now() + ... pub_date__gte=datetime.date.today() ... ).filter( ... pub_date__gte=datetime(2005, 1, 30) ... ) @@ -258,8 +258,8 @@ stored, used and reused. Example:: >> q1 = Entry.objects.filter(headline__startswith="What") - >> q2 = q1.exclude(pub_date__gte=datetime.now()) - >> q3 = q1.filter(pub_date__gte=datetime.now()) + >> q2 = q1.exclude(pub_date__gte=datetime.date.today()) + >> q3 = q1.filter(pub_date__gte=datetime.date.today()) These three ``QuerySets`` are separate. The first is a base :class:`~django.db.models.query.QuerySet` containing all entries that contain a @@ -282,7 +282,7 @@ actually run the query until the :class:`~django.db.models.query.QuerySet` is *evaluated*. Take a look at this example:: >>> q = Entry.objects.filter(headline__startswith="What") - >>> q = q.filter(pub_date__lte=datetime.now()) + >>> q = q.filter(pub_date__lte=datetime.date.today()) >>> q = q.exclude(body_text__icontains="food") >>> print(q) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 8159c8850c781..caff03c581ce9 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -311,18 +311,18 @@ model fields: to exclude from the form. For example, if you want a form for the ``Author`` model (defined -above) that includes only the ``name`` and ``title`` fields, you would +above) that includes only the ``name`` and ``birth_date`` fields, you would specify ``fields`` or ``exclude`` like this:: class PartialAuthorForm(ModelForm): class Meta: model = Author - fields = ('name', 'title') + fields = ('name', 'birth_date') class PartialAuthorForm(ModelForm): class Meta: model = Author - exclude = ('birth_date',) + exclude = ('title',) Since the Author model has only 3 fields, 'name', 'title', and 'birth_date', the forms above will contain exactly the same fields. diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 890c5e3195cbe..39b9a93c0472f 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -257,15 +257,14 @@ Installing the development version If you decide to use the latest development version of Django, you'll want to pay close attention to `the development timeline`_, - and you'll want to keep an eye on `the list of - backwards-incompatible changes`_. This will help you stay on top - of any new features you might want to use, as well as any changes + and you'll want to keep an eye on the :ref:`release notes for the + upcoming release `. This will help you stay + on top of any new features you might want to use, as well as any changes you'll need to make to your code when updating your copy of Django. (For stable releases, any necessary changes are documented in the release notes.) .. _the development timeline: https://code.djangoproject.com/timeline -.. _the list of backwards-incompatible changes: https://code.djangoproject.com/wiki/BackwardsIncompatibleChanges If you'd like to be able to update your Django code occasionally with the latest bug fixes and improvements, follow these instructions: diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 89d0c9f91fc42..f5749faaf2e18 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -324,8 +324,8 @@ Writing compatible code with six six_ is the canonical compatibility library for supporting Python 2 and 3 in a single codebase. Read its documentation! -:mod`six` is bundled with Django as of version 1.4.2. You can import it as -:mod`django.utils.six`. +:mod:`six` is bundled with Django as of version 1.4.2. You can import it as +:mod:`django.utils.six`. Here are the most common changes required to write compatible code. diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index 4f25d3dbfb4f8..57f45ab0ffae0 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -8,10 +8,11 @@ from django.test.utils import override_settings # local test models -from .admin import InnerInline +from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, - ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2) + ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, + Title) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -408,6 +409,47 @@ class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase): fixtures = ['admin-views-users.xml'] urls = "regressiontests.admin_inlines.urls" + def test_add_stackeds(self): + """ + Ensure that the "Add another XXX" link correctly adds items to the + stacked formset. + """ + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/holder4/add/')) + + inline_id = '#inner4stacked_set-group' + rows_length = lambda: len(self.selenium.find_elements_by_css_selector( + '%s .dynamic-inner4stacked_set' % inline_id)) + self.assertEqual(rows_length(), 3) + + add_button = self.selenium.find_element_by_link_text( + 'Add another Inner4 Stacked') + add_button.click() + + self.assertEqual(rows_length(), 4) + + def test_delete_stackeds(self): + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/holder4/add/')) + + inline_id = '#inner4stacked_set-group' + rows_length = lambda: len(self.selenium.find_elements_by_css_selector( + '%s .dynamic-inner4stacked_set' % inline_id)) + self.assertEqual(rows_length(), 3) + + add_button = self.selenium.find_element_by_link_text( + 'Add another Inner4 Stacked') + add_button.click() + add_button.click() + + self.assertEqual(rows_length(), 5, msg="sanity check") + for delete_link in self.selenium.find_elements_by_css_selector( + '%s .inline-deletelink' % inline_id): + delete_link.click() + self.assertEqual(rows_length(), 3) + def test_add_inlines(self): """ Ensure that the "Add another XXX" link correctly adds items to the @@ -516,6 +558,21 @@ def test_delete_inlines(self): self.assertEqual(len(self.selenium.find_elements_by_css_selector( 'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1) + def test_alternating_rows(self): + self.admin_login(username='super', password='secret') + self.selenium.get('%s%s' % (self.live_server_url, + '/admin/admin_inlines/profilecollection/add/')) + + # Add a few inlines + self.selenium.find_element_by_link_text('Add another Profile').click() + self.selenium.find_element_by_link_text('Add another Profile').click() + + row_selector = 'form#profilecollection_form tr.dynamic-profile_set' + self.assertEqual(len(self.selenium.find_elements_by_css_selector( + "%s.row1" % row_selector)), 2, msg="Expect two row1 styled rows") + self.assertEqual(len(self.selenium.find_elements_by_css_selector( + "%s.row2" % row_selector)), 1, msg="Expect one row2 styled row") + class SeleniumChromeTests(SeleniumFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index bc0f68456397d..6028eac846e9f 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -181,11 +181,11 @@ class DjangoAdminNoSettings(AdminScriptTestCase): "A series of tests for django-admin.py when there is no settings.py file." def test_builtin_command(self): - "no settings: django-admin builtin commands fail with an import error when no settings provided" + "no settings: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_bad_settings(self): "no settings: django-admin builtin commands fail if settings file (from argument) doesn't exist" @@ -213,11 +213,11 @@ def tearDown(self): self.remove_settings('settings.py') def test_builtin_command(self): - "default: django-admin builtin commands fail with an import error when no settings provided" + "default: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "default: django-admin builtin commands succeed if settings are provided as argument" @@ -279,11 +279,11 @@ def tearDown(self): self.remove_settings('settings.py') def test_builtin_command(self): - "fulldefault: django-admin builtin commands fail with an import error when no settings provided" + "fulldefault: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "fulldefault: django-admin builtin commands succeed if a settings file is provided" @@ -345,11 +345,11 @@ def tearDown(self): self.remove_settings('settings.py') def test_builtin_command(self): - "minimal: django-admin builtin commands fail with an import error when no settings provided" + "minimal: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "minimal: django-admin builtin commands fail if settings are provided as argument" @@ -411,11 +411,11 @@ def tearDown(self): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: django-admin builtin commands fail with an import error when no settings provided" + "alternate: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "alternate: django-admin builtin commands succeed if settings are provided as argument" @@ -482,11 +482,11 @@ def tearDown(self): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: django-admin builtin commands fail with an import error when no settings provided" + "alternate: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_settings(self): "alternate: django-admin builtin commands succeed if settings are provided as argument" @@ -570,11 +570,11 @@ def test_setup_environ_custom_template(self): self.assertTrue(os.path.exists(os.path.join(app_path, 'api.py'))) def test_builtin_command(self): - "directory: django-admin builtin commands fail with an import error when no settings provided" + "directory: django-admin builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_django_admin(args) self.assertNoOutput(out) - self.assertOutput(err, 'environment variable DJANGO_SETTINGS_MODULE is undefined') + self.assertOutput(err, 'settings are not configured') def test_builtin_with_bad_settings(self): "directory: django-admin builtin commands fail if settings file (from argument) doesn't exist" @@ -621,7 +621,7 @@ class ManageNoSettings(AdminScriptTestCase): "A series of tests for manage.py when there is no settings.py file." def test_builtin_command(self): - "no settings: manage.py builtin commands fail with an import error when no settings provided" + "no settings: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -786,7 +786,7 @@ def tearDown(self): self.remove_settings('settings.py') def test_builtin_command(self): - "minimal: manage.py builtin commands fail with an import error when no settings provided" + "minimal: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -852,7 +852,7 @@ def tearDown(self): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "alternate: manage.py builtin commands fail with an import error when no default settings provided" + "alternate: manage.py builtin commands fail with an error when no default settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) @@ -895,7 +895,7 @@ def test_custom_command(self): args = ['noargs_command'] out, err = self.run_manage(args) self.assertNoOutput(out) - self.assertOutput(err, "Unknown command: 'noargs_command'") + self.assertOutput(err, "Could not import settings 'regressiontests.settings'") def test_custom_command_with_settings(self): "alternate: manage.py can execute user commands if settings are provided as argument" @@ -927,7 +927,7 @@ def tearDown(self): self.remove_settings('alternate_settings.py') def test_builtin_command(self): - "multiple: manage.py builtin commands fail with an import error when no settings provided" + "multiple: manage.py builtin commands fail with an error when no settings provided" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args) self.assertNoOutput(out) diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index 293ddfebf6a70..fe291ebfb8ef6 100644 --- a/tests/regressiontests/admin_views/admin.py +++ b/tests/regressiontests/admin_views/admin.py @@ -27,11 +27,14 @@ Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, - RelatedPrepopulated, UndeletableObject) + RelatedPrepopulated, UndeletableObject, Simple) def callable_year(dt_value): - return dt_value.year + try: + return dt_value.year + except AttributeError: + return None callable_year.admin_order_field = 'date' @@ -575,6 +578,14 @@ def change_view(self, *args, **kwargs): return super(UndeletableObjectAdmin, self).change_view(*args, **kwargs) +def callable_on_unknown(obj): + return obj.unknown + + +class AttributeErrorRaisingAdmin(admin.ModelAdmin): + list_display = [callable_on_unknown, ] + + site = admin.AdminSite(name="admin") site.register(Article, ArticleAdmin) site.register(CustomArticle, CustomArticleAdmin) @@ -648,6 +659,7 @@ def change_view(self, *args, **kwargs): site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin) site.register(AdminOrderedCallable, AdminOrderedCallableAdmin) site.register(Color2, CustomTemplateFilterColorAdmin) +site.register(Simple, AttributeErrorRaisingAdmin) # Register core models we need in our tests from django.contrib.auth.models import User, Group diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py index 142527b022b11..031fb50f0f3b8 100644 --- a/tests/regressiontests/admin_views/customadmin.py +++ b/tests/regressiontests/admin_views/customadmin.py @@ -49,3 +49,4 @@ def queryset(self, request): site.register(models.ChapterXtra1, base_admin.ChapterXtra1Admin) site.register(User, UserLimitedAdmin) site.register(models.UndeletableObject, base_admin.UndeletableObjectAdmin) +site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 0d5e327ecff59..2c935c05a5de1 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -649,3 +649,9 @@ class UndeletableObject(models.Model): Refs #10057. """ name = models.CharField(max_length=255) + + +class Simple(models.Model): + """ + Simple model with nothing on it for use in testing + """ diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index cf7d4855fb3bf..9f56daa74393b 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -46,7 +46,7 @@ OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, - UndeletableObject) + Simple, UndeletableObject) ERROR_MESSAGE = "Please enter the correct username and password \ @@ -578,6 +578,20 @@ def test_change_view_with_show_delete_extra_context(self): (self.urlbit, instance.pk)) self.assertNotContains(response, 'deletelink') + def test_allows_attributeerror_to_bubble_up(self): + """ + Ensure that AttributeErrors are allowed to bubble when raised inside + a change list view. + + Requires a model to be created so there's something to be displayed + + Refs: #16655, #18593, and #18747 + """ + Simple.objects.create() + with self.assertRaises(AttributeError): + self.client.get('/test_admin/%s/admin_views/simple/' % self.urlbit) + + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewFormUrlTest(TestCase): urls = "regressiontests.admin_views.urls" diff --git a/tests/regressiontests/comment_tests/tests/__init__.py b/tests/regressiontests/comment_tests/tests/__init__.py index 40c0de0d57028..f80b29e17b855 100644 --- a/tests/regressiontests/comment_tests/tests/__init__.py +++ b/tests/regressiontests/comment_tests/tests/__init__.py @@ -17,7 +17,7 @@ @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',)) class CommentTestCase(TestCase): fixtures = ["comment_tests"] - urls = 'django.contrib.comments.urls' + urls = 'regressiontests.comment_tests.urls_default' def createSomeComments(self): # Two anonymous comments on two different objects diff --git a/tests/regressiontests/comment_tests/urls_default.py b/tests/regressiontests/comment_tests/urls_default.py new file mode 100644 index 0000000000000..e204f9ebcb674 --- /dev/null +++ b/tests/regressiontests/comment_tests/urls_default.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, include + +urlpatterns = patterns('', + (r'^', include('django.contrib.comments.urls')), + + # Provide the auth system login and logout views + (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), + (r'^accounts/logout/$', 'django.contrib.auth.views.logout'), +) diff --git a/tests/regressiontests/resolve_url/__init__.py b/tests/regressiontests/resolve_url/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/regressiontests/resolve_url/models.py b/tests/regressiontests/resolve_url/models.py new file mode 100644 index 0000000000000..238902edd271c --- /dev/null +++ b/tests/regressiontests/resolve_url/models.py @@ -0,0 +1,12 @@ +""" +Regression tests for the resolve_url function. +""" + +from django.db import models + + +class UnimportantThing(models.Model): + importance = models.IntegerField() + + def get_absolute_url(self): + return '/importance/%d/' % (self.importance,) diff --git a/tests/regressiontests/resolve_url/tests.py b/tests/regressiontests/resolve_url/tests.py new file mode 100644 index 0000000000000..d0bf44abde543 --- /dev/null +++ b/tests/regressiontests/resolve_url/tests.py @@ -0,0 +1,68 @@ +from __future__ import unicode_literals + +from django.core.urlresolvers import NoReverseMatch +from django.contrib.auth.views import logout +from django.utils.unittest import TestCase +from django.shortcuts import resolve_url + +from .models import UnimportantThing + + +class ResolveUrlTests(TestCase): + """ + Tests for the ``resolve_url`` function. + """ + + def test_url_path(self): + """ + Tests that passing a URL path to ``resolve_url`` will result in the + same url. + """ + self.assertEqual('/something/', resolve_url('/something/')) + + def test_full_url(self): + """ + Tests that passing a full URL to ``resolve_url`` will result in the + same url. + """ + url = 'http://example.com/' + self.assertEqual(url, resolve_url(url)) + + def test_model(self): + """ + Tests that passing a model to ``resolve_url`` will result in + ``get_absolute_url`` being called on that model instance. + """ + m = UnimportantThing(importance=1) + self.assertEqual(m.get_absolute_url(), resolve_url(m)) + + def test_view_function(self): + """ + Tests that passing a view name to ``resolve_url`` will result in the + URL path mapping to that view name. + """ + resolved_url = resolve_url(logout) + self.assertEqual('/accounts/logout/', resolved_url) + + def test_valid_view_name(self): + """ + Tests that passing a view function to ``resolve_url`` will result in + the URL path mapping to that view. + """ + resolved_url = resolve_url('django.contrib.auth.views.logout') + self.assertEqual('/accounts/logout/', resolved_url) + + def test_domain(self): + """ + Tests that passing a domain to ``resolve_url`` returns the same domain. + """ + self.assertEqual(resolve_url('example.com'), 'example.com') + + def test_non_view_callable_raises_no_reverse_match(self): + """ + Tests that passing a non-view callable into ``resolve_url`` raises a + ``NoReverseMatch`` exception. + """ + with self.assertRaises(NoReverseMatch): + resolve_url(lambda: 'asdf') + diff --git a/tests/regressiontests/utils/os_utils.py b/tests/regressiontests/utils/os_utils.py index a78f348cf56ca..a205d67431b63 100644 --- a/tests/regressiontests/utils/os_utils.py +++ b/tests/regressiontests/utils/os_utils.py @@ -1,21 +1,26 @@ +import os + from django.utils import unittest from django.utils._os import safe_join class SafeJoinTests(unittest.TestCase): def test_base_path_ends_with_sep(self): + drive, path = os.path.splitdrive(safe_join("/abc/", "abc")) self.assertEqual( - safe_join("/abc/", "abc"), - "/abc/abc", + path, + "{0}abc{0}abc".format(os.path.sep) ) def test_root_path(self): + drive, path = os.path.splitdrive(safe_join("/", "path")) self.assertEqual( - safe_join("/", "path"), - "/path", + path, + "{0}path".format(os.path.sep), ) + drive, path = os.path.splitdrive(safe_join("/", "")) self.assertEqual( - safe_join("/", ""), - "/", + path, + os.path.sep, ) diff --git a/tests/runtests.py b/tests/runtests.py index c548d2745b8d7..a81fee68582b1 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -92,7 +92,7 @@ def setup(verbosity, test_labels): settings.TEMPLATE_DIRS = (os.path.join(RUNTESTS_DIR, TEST_TEMPLATE_DIR),) settings.USE_I18N = True settings.LANGUAGE_CODE = 'en' - settings.LOGIN_URL = '/accounts/login/' + settings.LOGIN_URL = 'django.contrib.auth.views.login' settings.MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',